diff --git a/recipes/arcamax.recipe b/recipes/arcamax.recipe index 39fa199cc3..db4d753cef 100644 --- a/recipes/arcamax.recipe +++ b/recipes/arcamax.recipe @@ -6,12 +6,13 @@ __copyright__ = 'Copyright 2010 Starson17' www.arcamax.com ''' from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import Tag class Arcamax(BasicNewsRecipe): title = 'Arcamax' __author__ = 'Starson17' - __version__ = '1.03' - __date__ = '25 November 2010' + __version__ = '1.04' + __date__ = '18 April 2011' description = u'Family Friendly Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.' category = 'news, comics' language = 'en' @@ -30,8 +31,15 @@ class Arcamax(BasicNewsRecipe): , 'language' : language } - keep_only_tags = [dict(name='div', attrs={'class':['toon']}), - ] + keep_only_tags = [dict(name='div', attrs={'class':['comics-header']}), + dict(name='b', attrs={'class':['current']}), + dict(name='article', attrs={'class':['comic']}), + ] + + remove_tags = [dict(name='div', attrs={'id':['comicfull' ]}), + dict(name='div', attrs={'class':['calendar' ]}), + dict(name='nav', attrs={'class':['calendar-nav' ]}), + ] def parse_index(self): feeds = [] @@ -71,7 +79,6 @@ class Arcamax(BasicNewsRecipe): #(u"Rugrats", u"http://www.arcamax.com/rugrats"), (u"Speed Bump", u"http://www.arcamax.com/speedbump"), (u"Wizard of Id", u"http://www.arcamax.com/wizardofid"), - (u"Dilbert", u"http://www.arcamax.com/dilbert"), (u"Zits", u"http://www.arcamax.com/zits"), ]: articles = self.make_links(url) @@ -86,24 +93,37 @@ class Arcamax(BasicNewsRecipe): for page in pages: page_soup = self.index_to_soup(url) if page_soup: - title = page_soup.find(name='div', attrs={'class':'toon'}).p.img['alt'] + title = page_soup.find(name='div', attrs={'class':'comics-header'}).h1.contents[0] page_url = url - prev_page_url = 'http://www.arcamax.com' + page_soup.find('a', attrs={'class':'next'}, text='Previous').parent['href'] - current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':''}) + # orig prev_page_url = 'http://www.arcamax.com' + page_soup.find('a', attrs={'class':'prev'}, text='Previous').parent['href'] + prev_page_url = 'http://www.arcamax.com' + page_soup.find('span', text='Previous').parent.parent['href'] + date = self.tag_to_string(page_soup.find(name='b', attrs={'class':['current']})) + current_articles.append({'title': title, 'url': page_url, 'description':'', 'date': date}) url = prev_page_url current_articles.reverse() return current_articles def preprocess_html(self, soup): - main_comic = soup.find('p',attrs={'class':'m0'}) - if main_comic.a['target'] == '_blank': - main_comic.a.img['id'] = 'main_comic' + for img_tag in soup.findAll('img'): + parent_tag = img_tag.parent + if parent_tag.name == 'a': + new_tag = Tag(soup,'p') + new_tag.insert(0,img_tag) + parent_tag.replaceWith(new_tag) + elif parent_tag.name == 'p': + if not self.tag_to_string(parent_tag) == '': + new_div = Tag(soup,'div') + new_tag = Tag(soup,'p') + new_tag.insert(0,img_tag) + parent_tag.replaceWith(new_div) + new_div.insert(0,new_tag) + new_div.insert(1,parent_tag) 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#main_comic {max-width:100%; min-width:100%;} + 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;} ''' diff --git a/recipes/babyonline.recipe b/recipes/babyonline.recipe new file mode 100644 index 0000000000..0b892ed673 --- /dev/null +++ b/recipes/babyonline.recipe @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2011, Silviu Cotoar\u0103' +''' +babyonline.ro +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class BabyOnline(BasicNewsRecipe): + title = u'Baby Online' + __author__ = u'Silviu Cotoar\u0103' + description = u'De la p\u0103rinte la p\u0103rinte' + publisher = u'Baby Online' + oldest_article = 50 + language = 'ro' + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + category = 'Ziare,Reviste,Copii,Mame' + encoding = 'utf-8' + cover_url = 'http://www.babyonline.ro/images/default/logo.gif' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [ + dict(name='div', attrs={'id':'article_container'}) + ] + + remove_tags = [ + dict(name='div', attrs={'id':'bar_nav'}), + dict(name='div', attrs={'id':'service_send'}), + dict(name='div', attrs={'id':'other_videos'}), + dict(name='div', attrs={'class':'dot_line_yellow'}), + dict(name='a', attrs={'class':'print'}), + dict(name='a', attrs={'class':'email'}), + dict(name='a', attrs={'class':'YM'}), + dict(name='a', attrs={'class':'comment'}), + dict(name='div', attrs={'class':'tombstone_cross'}), + dict(name='span', attrs={'class':'liketext'}) + ] + + remove_tags_after = [ + dict(name='div', attrs={'id':'service_send'}) + ] + + feeds = [ + (u'Feeds', u'http://www.babyonline.ro/rss_homepage.xml') + ] + + def preprocess_html(self, soup): + return self.adeify_images(soup) diff --git a/recipes/daily_telegraph.recipe b/recipes/daily_telegraph.recipe index 5e1a2f7bfb..5ee48f3f79 100644 --- a/recipes/daily_telegraph.recipe +++ b/recipes/daily_telegraph.recipe @@ -61,6 +61,12 @@ class DailyTelegraph(BasicNewsRecipe): (u'Entertainment News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_news_201.xml'), (u'Lifestyle News', u'http://feeds.news.com.au/public/rss/2.0/dtele_lifestyle_227.xml'), (u'Music', u'http://feeds.news.com.au/public/rss/2.0/dtele_music_441.xml'), + (u'Sport', + u'http://feeds.news.com.au/public/rss/2.0/dtele_sport_203.xml'), + (u'Soccer', + u'http://feeds.news.com.au/public/rss/2.0/dtele_sports_soccer_344.xml'), + (u'Rugby Union', + u'http://feeds.news.com.au/public/rss/2.0/dtele_sports_rugby_union_342.xml'), (u'Property Confidential', u'http://feeds.news.com.au/public/rss/2.0/dtele_property_confidential_463.xml'), (u'Property - Your Space', u'http://feeds.news.com.au/public/rss/2.0/dtele_property_yourspace_462.xml'), (u'Confidential News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_confidential_252.xml'), diff --git a/recipes/der_spiegel.recipe b/recipes/der_spiegel.recipe new file mode 100644 index 0000000000..1e94785233 --- /dev/null +++ b/recipes/der_spiegel.recipe @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2011, Nikolas Mangold ' +''' +spiegel.de +''' +from calibre.web.feeds.news import BasicNewsRecipe +from calibre import strftime +from calibre import re + +class DerSpiegel(BasicNewsRecipe): + title = 'Der Spiegel' + __author__ = 'Nikolas Mangold' + description = 'Der Spiegel, Printed Edition. Access to paid content.' + publisher = 'SPIEGEL-VERLAG RUDOLF AUGSTEIN GMBH & CO. KG' + category = 'news, politics, Germany' + no_stylesheets = True + encoding = 'cp1252' + needs_subscription = True + remove_empty_feeds = True + delay = 1 + PREFIX = 'http://m.spiegel.de' + INDEX = PREFIX + '/spiegel/print/epaper/index-heftaktuell.html' + use_embedded_content = False + masthead_url = 'http://upload.wikimedia.org/wikipedia/en/thumb/1/17/Der_Spiegel_logo.svg/200px-Der_Spiegel_logo.svg.png' + language = 'de' + publication_type = 'magazine' + extra_css = ' body{font-family: Arial,Helvetica,sans-serif} ' + timefmt = '[%W/%Y]' + empty_articles = ['Titelbild'] + preprocess_regexps = [ + (re.compile(r'

', re.DOTALL|re.IGNORECASE), lambda match: '
'), + ] + + def get_browser(self): + def has_login_name(form): + try: + form.find_control(name="f.loginName") + except: + return False + else: + return True + + br = BasicNewsRecipe.get_browser() + if self.username is not None and self.password is not None: + br.open(self.PREFIX + '/meinspiegel/login.html') + br.select_form(predicate=has_login_name) + br['f.loginName' ] = self.username + br['f.password'] = self.password + br.submit() + return br + + remove_tags_before = dict(attrs={'class':'spArticleContent'}) + remove_tags_after = dict(attrs={'class':'spArticleCredit'}) + + def parse_index(self): + soup = self.index_to_soup(self.INDEX) + + cover = soup.find('img', width=248) + if cover is not None: + self.cover_url = cover['src'] + + index = soup.find('dl') + + feeds = [] + for section in index.findAll('dt'): + section_title = self.tag_to_string(section).strip() + self.log('Found section ', section_title) + + articles = [] + for article in section.findNextSiblings(['dd','dt']): + if article.name == 'dt': + break + link = article.find('a') + title = self.tag_to_string(link).strip() + if title in self.empty_articles: + continue + self.log('Found article ', title) + url = self.PREFIX + link['href'] + articles.append({'title' : title, 'date' : strftime(self.timefmt), 'url' : url}) + feeds.append((section_title,articles)) + return feeds; diff --git a/recipes/ecuisine.recipe b/recipes/ecuisine.recipe index 53631e0b14..77d761e653 100644 --- a/recipes/ecuisine.recipe +++ b/recipes/ecuisine.recipe @@ -14,14 +14,14 @@ class EcuisineRo(BasicNewsRecipe): __author__ = u'Silviu Cotoar\u0103' description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti' publisher = 'eCuisine' - oldest_article = 5 + oldest_article = 50 language = 'ro' max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False category = 'Ziare,Retete,Bucatarie' encoding = 'utf-8' - cover_url = '' + cover_url = 'http://www.ecuisine.ro/sites/all/themes/ecuisine/images/logo.gif' conversion_options = { 'comments' : description @@ -31,8 +31,8 @@ class EcuisineRo(BasicNewsRecipe): } keep_only_tags = [ - dict(name='div', attrs={'class':'page-title'}) - , dict(name='div', attrs={'class':'content clearfix'}) + dict(name='h1', attrs={'id':'page-title'}) + , dict(name='div', attrs={'class':'field-item even'}) ] remove_tags = [ diff --git a/recipes/egirl.recipe b/recipes/egirl.recipe index b456323db9..56d515669d 100644 --- a/recipes/egirl.recipe +++ b/recipes/egirl.recipe @@ -31,8 +31,8 @@ class EgirlRo(BasicNewsRecipe): } keep_only_tags = [ - dict(name='div', attrs={'id':'title_art'}) - , dict(name='div', attrs={'class':'content_style'}) + dict(name='div', attrs={'id':'content_art'}) + , dict(name='div', attrs={'class':'content_articol'}) ] feeds = [ diff --git a/recipes/handelsblatt.recipe b/recipes/handelsblatt.recipe index 945dac0560..056fcfb26b 100644 --- a/recipes/handelsblatt.recipe +++ b/recipes/handelsblatt.recipe @@ -1,4 +1,3 @@ - from calibre.web.feeds.news import BasicNewsRecipe class Handelsblatt(BasicNewsRecipe): @@ -7,14 +6,11 @@ class Handelsblatt(BasicNewsRecipe): oldest_article = 7 max_articles_per_feed = 100 no_stylesheets = True - cover_url = 'http://www.handelsblatt.com/images/logo/logo_handelsblatt.com.png' +# cover_url = 'http://www.handelsblatt.com/images/logo/logo_handelsblatt.com.png' language = 'de' - # keep_only_tags = [] - keep_only_tags = (dict(name = 'div', attrs = {'class': ['hcf-detail-abstract hcf-teaser ajaxify','hcf-detail','hcf-author-wrapper']})) - # keep_only_tags.append(dict(name = 'div', attrs = {'id': 'fullText'})) - remove_tags = [dict(name='img', attrs = {'src': 'http://www.handelsblatt.com/images/icon/loading.gif'}) - ,dict(name='ul' , attrs={'class':['hcf-detail-tools']}) - ] + + remove_tags_before = dict(attrs={'class':'hcf-overline'}) + remove_tags_after = dict(attrs={'class':'hcf-footer'}) feeds = [ (u'Handelsblatt Exklusiv',u'http://www.handelsblatt.com/rss/exklusiv'), @@ -28,17 +24,16 @@ class Handelsblatt(BasicNewsRecipe): (u'Handelsblatt Magazin',u'http://www.handelsblatt.com/rss/magazin/'), (u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs') ] + extra_css = ''' - .hcf-headline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} - .hcf-overline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} - .hcf-exclusive {font-family:Arial,Helvetica,sans-serif; font-style:italic;font-weight:bold; margin-right:5pt;} - p{font-family:Arial,Helvetica,sans-serif;} - .hcf-location-mark{font-weight:bold; margin-right:5pt;} - .MsoNormal{font-family:Helvetica,Arial,sans-serif;} - .hcf-author-wrapper{font-style:italic;} - .hcf-article-date{font-size:x-small;} - .hcf-caption {font-style:italic;font-size:small;} - img {align:left;} + 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;} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} ''' - + def print_version(self, url): + url = url.split('/') + url[-1] = 'v_detail_tab_print,'+url[-1] + url = '/'.join(url) + return url diff --git a/recipes/icons/babyonline.png b/recipes/icons/babyonline.png new file mode 100644 index 0000000000..030c611d88 Binary files /dev/null and b/recipes/icons/babyonline.png differ diff --git a/recipes/ilsole24ore.recipe b/recipes/ilsole24ore.recipe index 920c703222..0cf1ddc6bf 100644 --- a/recipes/ilsole24ore.recipe +++ b/recipes/ilsole24ore.recipe @@ -1,71 +1,65 @@ -#!/usr/bin/env python -__license__ = 'GPL v3' -__author__ = 'Lorenzo Vigentini & Edwin van Maastrigt' -__copyright__ = '2009, Lorenzo Vigentini and Edwin van Maastrigt ' -__description__ = 'Financial news daily paper - v1.02 (30, January 2010)' +__author__ = 'Marco Saraceno' +__copyright__ = '2010, Marco Saraceno ' +description = 'Italian daily newspaper - v 1.1 (Mar14,2011)' ''' -http://www.ilsole24ore.com/ +http://www.ilsole24ore.com ''' from calibre.web.feeds.news import BasicNewsRecipe +class IlSole24Ore(BasicNewsRecipe): + __author__ = 'Marco Saraceno' + description = 'Italian financial daily newspaper' -class ilsole24Ore(BasicNewsRecipe): - author = 'Lorenzo Vigentini & Edwin van Maastrigt' - description = 'Financial news daily paper' - - cover_url = 'http://www.ilsole24ore.com/img2007/print_header.gif' - - title = u'il Sole 24 Ore New' - publisher = 'italiaNews' - category = 'News, finance, economy, politics' + cover_url = 'http://www.shopping24.ilsole24ore.com/ProductRelated/rds/img/logo_sole.gif' + title = u'Il Sole 24 Ore' + publisher = 'Gruppo editoriale GRUPPO 24ORE' + category = 'News, politics, culture, economy, financial, Italian' language = 'it' timefmt = '[%a, %d %b, %Y]' oldest_article = 2 - max_articles_per_feed = 50 + max_articles_per_feed = 100 use_embedded_content = False + extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }' + + + remove_tags = [ + dict(name='div', attrs={'class':['header','titolo']}), + dict(name='table', attrs={'class':['footer1024','footerdown']}), + ] - remove_javascript = True - no_stylesheets = True def get_article_url(self, article): - return article.get('id', article.get('guid', None)) + link = article.get('link', None) + if link is None: + return article + if link.split('/')[-1]=="story01.htm": + link=link.split('/')[-2] + a=['0B','0C','0D','0E','0F','0G','0N' ,'0L0S','0A'] + b=['.' ,'/' ,'?' ,'-' ,'=' ,'&' ,'.com','www.','0'] + for i in range(0,len(a)): + link=link.replace(a[i],b[i]) + link="http://"+link + return link + + feeds = [ + (u'Notizie Italia', u'http://www.ilsole24ore.com/rss/notizie/italia.xml'), + (u'Notizie Europa', u'http://www.ilsole24ore.com/rss/notizie/europa.xml'), + (u'Notizie USA', u'http://www.ilsole24ore.com/rss/notizie/usa.xml'), + (u'Notizie Americhe', u'http://www.ilsole24ore.com/rss/notizie/americhe.xml'), + (u'Notizie Medio Oriente e Africa', u'http://www.ilsole24ore.com/rss/notizie/medio-oriente-e-africa.xml'), + (u'Notizie Asia e Oceania', u'http://www.ilsole24ore.com/rss/notizie/asia-e-oceania.xml'), + (u'Commenti', u'http://www.ilsole24ore.com/rss/commenti-e-idee.xml'), + (u'Norme e tributi', u'http://www.ilsole24ore.com/rss/norme-e-tributi.xml'), + (u'Finanza', u'http://www.ilsole24ore.com/rss/finanza-e-mercati.xml'), + (u'Economia', u'http://www.ilsole24ore.com/rss/economia.xml'), + (u'Tecnologia', u'http://www.ilsole24ore.com/rss/tecnologie.xml'), + (u'Cultura', u'http://www.ilsole24ore.com/rss/cultura.xml'), + ] def print_version(self, url): - link, sep, params = url.rpartition('?') - if link is None: - return link.replace('_1.php', '_php') - return link.replace('.shtml', '_PRN.shtml') - - keep_only_tags = [ - dict(name='div', attrs={'class':'txt'}) - ] -# remove_tags = [dict(name='br')] - - feeds = [ - (u'Prima pagina', u'http://www.ilsole24ore.com/rss/primapagina.xml'), - (u'Norme e tributi', u'http://www.ilsole24ore.com/rss/norme-tributi.xml'), - (u'Finanza e mercati', u'http://www.ilsole24ore.com/rss/finanza-mercati.xml'), - (u'Economia e lavoro', u'http://www.ilsole24ore.com/rss/economia-lavoro.xml'), - (u'Italia', u'http://www.ilsole24ore.com/rss/italia.xml'), - (u'Mondo', u'http://www.ilsole24ore.com/rss/mondo.xml'), - (u'Tecnologia e business', u'http://www.ilsole24ore.com/rss/tecnologia-business.xml'), - (u'Cultura e tempo libero', u'http://www.ilsole24ore.com/rss/tempolibero-cultura.xml'), - (u'Sport', u'http://www.ilsole24ore.com/rss/sport.xml'), - (u'Professionisti 24', u'http://www.ilsole24ore.com/rss/prof_home.xml'), - (u'Ambiente e Sicurezza',u'http://www.ilsole24ore.com/rss/prof_as.xml') - ] - - extra_css = ''' - html, body, table, tr, td, h1, h2, h3, h4, h5, h6, p, a, span, br, img {margin:0;padding:0;border:0;font-size:12px;font-family:"Georgia","Times New Roman";} - .linkHighlight {color:#0292c6;} - .txt {border-bottom:1px solid #7c7c7c;padding-bottom:20px};text-align:justify;font-family:"serif"} - .txt p {line-height:18px;} - .txt span {line-height:22px;} - .title h3 {color:#7b7b7b;} - .title h4 {color:#08526e;font-size:26px;font-family:"Times New Roman";font-weight:normal;} - ''' + return url.replace('.shtml', '_PRN.shtml') diff --git a/recipes/newsweek.recipe b/recipes/newsweek.recipe index 73837c1872..97abd69aac 100644 --- a/recipes/newsweek.recipe +++ b/recipes/newsweek.recipe @@ -1,4 +1,3 @@ -import string from calibre.web.feeds.news import BasicNewsRecipe class Newsweek(BasicNewsRecipe): @@ -11,7 +10,6 @@ class Newsweek(BasicNewsRecipe): no_stylesheets = True BASE_URL = 'http://www.newsweek.com' - INDEX = BASE_URL+'/topics.html' keep_only_tags = dict(name='article', attrs={'class':'article-text'}) remove_tags = [dict(attrs={'data-dartad':True})] @@ -23,11 +21,14 @@ class Newsweek(BasicNewsRecipe): return soup def newsweek_sections(self): - soup = self.index_to_soup(self.INDEX) - for a in soup.findAll('a', title='Primary tag', href=True): - yield (string.capitalize(self.tag_to_string(a)), - self.BASE_URL+a['href']) - + return [ + ('Nation', 'http://www.newsweek.com/tag/nation.html'), + ('Society', 'http://www.newsweek.com/tag/society.html'), + ('Culture', 'http://www.newsweek.com/tag/culture.html'), + ('World', 'http://www.newsweek.com/tag/world.html'), + ('Politics', 'http://www.newsweek.com/tag/politics.html'), + ('Business', 'http://www.newsweek.com/tag/business.html'), + ] def newsweek_parse_section_page(self, soup): for article in soup.findAll('article', about=True, diff --git a/recipes/slashdot.recipe b/recipes/slashdot.recipe index c7c68c3f1a..b10700a749 100644 --- a/recipes/slashdot.recipe +++ b/recipes/slashdot.recipe @@ -8,23 +8,36 @@ __copyright__ = '2009, Kovid Goyal edited by Huan T' from calibre.web.feeds.news import BasicNewsRecipe class Slashdot(BasicNewsRecipe): - title = u'Slashdot.org' - description = '''Tech news. WARNING: This recipe downloads a lot - of content and may result in your IP being banned from slashdot.org''' - oldest_article = 7 - simultaneous_downloads = 1 - delay = 3 - max_articles_per_feed = 100 - language = 'en' + title = u'Slashdot.org' + description = '''Tech news. WARNING: This recipe downloads a lot + of content and may result in your IP being banned from slashdot.org''' + oldest_article = 7 + simultaneous_downloads = 1 + delay = 3 + max_articles_per_feed = 100 + language = 'en' - __author__ = 'floweros edited by Huan T' - no_stylesheets = True -# keep_only_tags = [ -# dict(name='div',attrs={'class':'article'}), -# dict(name='div',attrs={'class':'commentTop'}), -# ] + __author__ = 'floweros edited by Huan T' + no_stylesheets = True + keep_only_tags = [ + dict(name='div',attrs={'id':'article'}), + dict(name='div',attrs={'class':['postBody' 'details']}), + dict(name='footer',attrs={'class':['clearfix meta article-foot']}), + dict(name='article',attrs={'class':['fhitem fhitem-story article usermode thumbs grid_24']}), + dict(name='dl',attrs={'class':'relatedPosts'}), + dict(name='h2',attrs={'class':'story'}), + dict(name='span',attrs={'class':'comments'}), + ] - feeds = [ + + remove_tags = [ + dict(name='aside',attrs={'id':'slashboxes'}), + dict(name='div',attrs={'class':'paginate'}), + dict(name='section',attrs={'id':'comments'}), + dict(name='span',attrs={'class':'topic'}), + ] + + feeds = [ (u'Slashdot', u'http://rss.slashdot.org/Slashdot/slashdot'), (u'/. IT', @@ -37,5 +50,3 @@ class Slashdot(BasicNewsRecipe): u'http://rss.slashdot.org/Slashdot/slashdotYourRightsOnline') ] - def get_article_url(self, article): - return article.get('feedburner_origlink', None) diff --git a/recipes/tabu.recipe b/recipes/tabu.recipe index f98ed8a155..941c491c79 100644 --- a/recipes/tabu.recipe +++ b/recipes/tabu.recipe @@ -37,10 +37,12 @@ class TabuRo(BasicNewsRecipe): ] remove_tags = [ - dict(name='div', attrs={'class':'asemanatoare'}) + dict(name='div', attrs={'class':'asemanatoare'}), + dict(name='div', attrs={'class':'social'}) ] remove_tags_after = [ + dict(name='div', attrs={'class':'social'}), dict(name='div', attrs={'id':'comments'}), dict(name='div', attrs={'class':'asemanatoare'}) ] diff --git a/recipes/the_journal.recipe b/recipes/the_journal.recipe new file mode 100644 index 0000000000..e65d7e272e --- /dev/null +++ b/recipes/the_journal.recipe @@ -0,0 +1,26 @@ +__license__ = 'GPL v3' +__copyright__ = '2011 Phil Burns' +''' +TheJournal.ie +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class TheJournal(BasicNewsRecipe): + + __author_ = 'Phil Burns' + title = u'TheJournal.ie' + oldest_article = 1 + max_articles_per_feed = 100 + encoding = 'utf8' + language = 'en_IE' + timefmt = ' (%A, %B %d, %Y)' + + no_stylesheets = True + remove_tags = [dict(name='div', attrs={'class':'footer'}), + dict(name=['script', 'noscript'])] + + extra_css = 'p, div { margin: 0pt; border: 0pt; text-indent: 0.5em }' + + feeds = [ + (u'Latest News', u'http://www.thejournal.ie/feed/')] diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index c4c951f980..091aa9a34d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -48,7 +48,7 @@ authors_completer_append_separator = False # When this tweak is changed, the author_sort values stored with each author # must be recomputed by right-clicking on an author in the left-hand tags pane, # selecting 'manage authors', and pressing 'Recalculate all author sort values'. -author_sort_copy_method = 'invert' +author_sort_copy_method = 'comma' #: Use author sort in Tag Browser # Set which author field to display in the tags pane (the list of authors, diff --git a/resources/images/id_card.png b/resources/images/id_card.png deleted file mode 100644 index 80ac5fda11..0000000000 Binary files a/resources/images/id_card.png and /dev/null differ diff --git a/resources/images/identifiers.png b/resources/images/identifiers.png new file mode 100644 index 0000000000..17906dd2d2 Binary files /dev/null and b/resources/images/identifiers.png differ diff --git a/resources/template-functions.json b/resources/template-functions.json index c19627c6c7..cf858c7691 100644 --- a/resources/template-functions.json +++ b/resources/template-functions.json @@ -5,6 +5,7 @@ "strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n", "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n", "ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n", + "booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n", "select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n", "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n", "subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n", @@ -25,9 +26,9 @@ "capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n", "count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n", "lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n", - "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", - "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n", + "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", + "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", "raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n", "cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n" } \ No newline at end of file diff --git a/setup.py b/setup.py index d8bd0267ee..1424d83137 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,9 @@ from setup import prints, get_warnings def check_version_info(): vi = sys.version_info - if vi[0] == 2 and vi[1] > 5: + if vi[0] == 2 and vi[1] > 6: return None - return 'calibre requires python >= 2.6' + return 'calibre requires python >= 2.7 and < 3' def option_parser(): parser = optparse.OptionParser() diff --git a/setup/__init__.py b/setup/__init__.py index 9e62fb377d..61bafd2282 100644 --- a/setup/__init__.py +++ b/setup/__init__.py @@ -24,8 +24,10 @@ def initialize_constants(): global __version__, __appname__, modules, functions, basenames, scripts src = open('src/calibre/constants.py', 'rb').read() - __version__ = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) - __appname__ = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) + nv = re.search(r'numeric_version\s+=\s+\((\d+), (\d+), (\d+)\)', src) + __version__ = '%s.%s.%s'%(nv.group(1), nv.group(2), nv.group(3)) + __appname__ = re.search(r'__appname__\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]', + src).group(2) epsrc = re.compile(r'entry_points = (\{.*?\})', re.DOTALL).\ search(open('src/calibre/linux.py', 'rb').read()).group(1) entry_points = eval(epsrc, {'__appname__': __appname__}) diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index cf4dcd5f9d..f666427598 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -13,7 +13,8 @@ from setup import Command, modules, functions, basenames, __version__, \ from setup.build_environment import msvc, MT, RC from setup.installer.windows.wix import WixMixIn -QT_DIR = 'Q:\\Qt\\4.7.1' +OPENSSL_DIR = r'Q:\openssl' +QT_DIR = 'Q:\\Qt\\4.7.2' QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns'] LIBUSB_DIR = 'C:\\libusb' LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll' @@ -108,6 +109,8 @@ class Win32Freeze(Command, WixMixIn): self.dll_dir = self.j(self.base, 'DLLs') shutil.copytree(r'C:\Python%s\DLLs'%self.py_ver, self.dll_dir, ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*')) + for x in glob.glob(self.j(OPENSSL_DIR, 'bin', '*.dll')): + shutil.copy2(x, self.dll_dir) for x in QT_DLLS: x += '4.dll' if not x.startswith('phonon'): x = 'Qt'+x diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 5dfd956ce2..ce6ca650a4 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -53,12 +53,25 @@ SQLite Put sqlite3*.h from the sqlite windows amlgamation in ~/sw/include +OpenSSL +-------- + +First install ActiveState Perl if you dont already have perl in windows +Download and untar the openssl tarball, follow the instructions in INSTALL.W32 (use no-asm) +to install use prefix q:\openssl + +perl Configure VC-WIN32 no-asm enable-static-engine --prefix=Q:/openssl +ms\do_ms.bat +nmake -f ms\ntdll.mak +nmake -f ms\ntdll.mak test +nmake -f ms\ntdll.mak install + Qt -------- Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make:: - configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs && nmake + configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake SIP ----- diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 0fddb9de9d..29c69a6799 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -3,11 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import uuid, sys, os, re, logging, time, random, \ - __builtin__, warnings, multiprocessing -from contextlib import closing -from urllib import getproxies -from urllib2 import unquote as urllib2_unquote +import sys, os, re, time, random, __builtin__, warnings __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) from htmlentitydefs import name2codepoint from math import floor @@ -16,25 +12,51 @@ from functools import partial warnings.simplefilter('ignore', DeprecationWarning) -from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \ - terminal_controller, preferred_encoding, \ - __appname__, __version__, __author__, \ - win32event, win32api, winerror, fcntl, \ - filesystem_encoding, plugins, config_dir -from calibre.startup import winutil, winutilerror, guess_type +from calibre.constants import (iswindows, isosx, islinux, isfreebsd, isfrozen, + preferred_encoding, __appname__, __version__, __author__, + win32event, win32api, winerror, fcntl, + filesystem_encoding, plugins, config_dir) +from calibre.startup import winutil, winutilerror -if islinux and not getattr(sys, 'frozen', False): - # Imported before PyQt4 to workaround PyQt4 util-linux conflict on gentoo +if False and islinux and not getattr(sys, 'frozen', False): + # Imported before PyQt4 to workaround PyQt4 util-linux conflict discovered on gentoo + # See http://bugs.gentoo.org/show_bug.cgi?id=317557 + # Importing uuid is slow so get rid of this at some point, maybe in a few + # years when even Debian has caught up + # Also remember to remove it from site.py in the binary builds + import uuid uuid.uuid4() if False: # Prevent pyflakes from complaining winutil, winutilerror, __appname__, islinux, __version__ - fcntl, win32event, isfrozen, __author__, terminal_controller - winerror, win32api, isfreebsd, guess_type + fcntl, win32event, isfrozen, __author__ + winerror, win32api, isfreebsd -import cssutils -cssutils.log.setLevel(logging.WARN) +_mt_inited = False +def _init_mimetypes(): + global _mt_inited + import mimetypes + mimetypes.init([P('mime.types')]) + _mt_inited = True + +def guess_type(*args, **kwargs): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.guess_type(*args, **kwargs) + +def guess_all_extensions(*args, **kwargs): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.guess_all_extensions(*args, **kwargs) + +def get_types_map(): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.types_map def to_unicode(raw, encoding='utf-8', errors='strict'): if isinstance(raw, unicode): @@ -182,6 +204,7 @@ class CommandLineError(Exception): pass def setup_cli_handlers(logger, level): + import logging if os.environ.get('CALIBRE_WORKER', None) is not None and logger.handlers: return logger.setLevel(level) @@ -243,6 +266,7 @@ def extract(path, dir): extractor(path, dir) def get_proxies(debug=True): + from urllib import getproxies proxies = getproxies() for key, proxy in list(proxies.items()): if not proxy or '..' in proxy: @@ -386,6 +410,7 @@ class StreamReadWrapper(object): def detect_ncpus(): """Detects the number of effective CPUs in the system""" + import multiprocessing ans = -1 try: ans = multiprocessing.cpu_count() @@ -550,6 +575,9 @@ def get_download_filename(url, cookie_file=None): ''' Get a local filename for a URL using the content disposition header ''' + from contextlib import closing + from urllib2 import unquote as urllib2_unquote + filename = '' br = browser() @@ -679,4 +707,3 @@ main() ipshell() sys.argv = old_argv - diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 6f26a63940..86dd1ada3b 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -1,28 +1,32 @@ +from future_builtins import map + __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -__appname__ = 'calibre' -__version__ = '0.7.56' -__author__ = "Kovid Goyal " - -import re, importlib -_ver = __version__.split('.') -_ver = [int(re.search(r'(\d+)', x).group(1)) for x in _ver] -numeric_version = tuple(_ver) +__appname__ = u'calibre' +numeric_version = (0, 7, 56) +__version__ = u'.'.join(map(unicode, numeric_version)) +__author__ = u"Kovid Goyal " ''' Various run time constants. ''' -import sys, locale, codecs, os -from calibre.utils.terminfo import TerminalController +import sys, locale, codecs, os, importlib, collections -terminal_controller = TerminalController(sys.stdout) +_tc = None +def terminal_controller(): + global _tc + if _tc is None: + from calibre.utils.terminfo import TerminalController + _tc = TerminalController(sys.stdout) + return _tc -iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() -isosx = 'darwin' in sys.platform.lower() -isnewosx = isosx and getattr(sys, 'new_app_bundle', False) -isfreebsd = 'freebsd' in sys.platform.lower() +_plat = sys.platform.lower() +iswindows = 'win32' in _plat or 'win64' in _plat +isosx = 'darwin' in _plat +isnewosx = isosx and getattr(sys, 'new_app_bundle', False) +isfreebsd = 'freebsd' in _plat islinux = not(iswindows or isosx or isfreebsd) isfrozen = hasattr(sys, 'frozen') isunix = isosx or islinux @@ -41,6 +45,7 @@ fcntl = None if iswindows else importlib.import_module('fcntl') filesystem_encoding = sys.getfilesystemencoding() if filesystem_encoding is None: filesystem_encoding = 'utf-8' + DEBUG = False def debug(): @@ -48,15 +53,12 @@ def debug(): DEBUG = True # plugins {{{ -plugins = None -if plugins is None: - # Load plugins - def load_plugins(): - plugins = {} - plugin_path = sys.extensions_location - sys.path.insert(0, plugin_path) - for plugin in [ +class Plugins(collections.Mapping): + + def __init__(self): + self._plugins = {} + plugins = [ 'pictureflow', 'lzx', 'msdes', @@ -70,19 +72,44 @@ if plugins is None: 'chm_extra', 'icu', 'speedup', - ] + \ - (['winutil'] if iswindows else []) + \ - (['usbobserver'] if isosx else []): - try: - p, err = importlib.import_module(plugin), '' - except Exception as err: - p = None - err = str(err) - plugins[plugin] = (p, err) - sys.path.remove(plugin_path) - return plugins + ] + if iswindows: + plugins.append('winutil') + if isosx: + plugins.append('usbobserver') + self.plugins = frozenset(plugins) - plugins = load_plugins() + def load_plugin(self, name): + if name in self._plugins: + return + sys.path.insert(0, sys.extensions_location) + try: + p, err = importlib.import_module(name), '' + except Exception as err: + p = None + err = str(err) + self._plugins[name] = (p, err) + sys.path.remove(sys.extensions_location) + + def __iter__(self): + return iter(self.plugins) + + def __len__(self): + return len(self.plugins) + + def __contains__(self, name): + return name in self.plugins + + def __getitem__(self, name): + if name not in self.plugins: + raise KeyError('No plugin named %r'%name) + self.load_plugin(name) + return self._plugins[name] + + +plugins = None +if plugins is None: + plugins = Plugins() # }}} # config_dir {{{ diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d3b0b8409d..00af4e5117 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \ from calibre.constants import numeric_version from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf -from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.utils.config import test_eight_code # To archive plugins {{{ @@ -98,6 +97,8 @@ class TXT2TXTZ(FileTypePlugin): on_import = True def _get_image_references(self, txt, base_dir): + from calibre.ebooks.oeb.base import OEB_IMAGES + images = [] # Textile @@ -626,8 +627,9 @@ if test_eight_code: from calibre.ebooks.metadata.sources.amazon import Amazon from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary from calibre.ebooks.metadata.sources.isbndb import ISBNDB + from calibre.ebooks.metadata.sources.overdrive import OverDrive - plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB] + plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive] # }}} else: diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index c58f36524e..d3ecab7f16 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -22,6 +22,11 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \ from calibre.ebooks.epub.fix import ePubFixer from calibre.ebooks.metadata.sources.base import Source +builtin_names = frozenset([p.name for p in builtin_plugins]) + +class NameConflict(ValueError): + pass + def _config(): c = Config('customize') c.add_opt('plugins', default={}, help=_('Installed plugins')) @@ -355,6 +360,9 @@ def set_file_type_metadata(stream, mi, ftype): def add_plugin(path_to_zip_file): make_config_dir() plugin = load_plugin(path_to_zip_file) + if plugin.name in builtin_names: + raise NameConflict( + 'A builtin plugin with the name %r already exists' % plugin.name) plugin = initialize_plugin(plugin, path_to_zip_file) plugins = config['plugins'] zfp = os.path.join(plugin_dir, plugin.name+'.zip') @@ -506,7 +514,11 @@ def initialize_plugin(plugin, path_to_zip_file): def initialize_plugins(): global _initialized_plugins _initialized_plugins = [] - for zfp in list(config['plugins'].values()) + builtin_plugins: + conflicts = [name for name in config['plugins'] if name in + builtin_names] + for p in conflicts: + remove_plugin(p) + for zfp in list(config['plugins'].itervalues()) + builtin_plugins: try: try: plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 86a0477811..8d65c37bbf 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -106,7 +106,7 @@ def migrate(old, new): from calibre.library.database import LibraryDatabase from calibre.library.database2 import LibraryDatabase2 from calibre.utils.terminfo import ProgressBar - from calibre import terminal_controller + from calibre.constants import terminal_controller class Dummy(ProgressBar): def setLabelText(self, x): pass def setAutoReset(self, y): pass @@ -119,7 +119,7 @@ def migrate(old, new): db = LibraryDatabase(old) db2 = LibraryDatabase2(new) - db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...')) + db2.migrate_old(db, Dummy(terminal_controller(), 'Migrating database...')) prefs['library_path'] = os.path.abspath(new) print 'Database migrated to', os.path.abspath(new) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 44d9bc1e49..7fe246f450 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -108,10 +108,10 @@ class ANDROID(USBMS): 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2', - 'MB860', 'MULTI-CARD', 'MID7015A'] + 'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', - 'A70S', 'A101IT', '7'] + 'A70S', 'A101IT', '7', 'INCREDIBLE'] OSX_MAIN_MEM = 'Android Device Main Memory' diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index c63eada0c8..9b729a3561 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -8,7 +8,7 @@ manner. import sys, os, re from threading import RLock -from calibre import iswindows, isosx, plugins, islinux +from calibre.constants import iswindows, isosx, plugins, islinux osx_scanner = win_scanner = linux_scanner = None diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index a56abb907e..d5b214884e 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -7,7 +7,7 @@ Code for the conversion of ebook formats and the reading of metadata from various formats. ''' -import traceback, os +import traceback, os, re from calibre import CurrentDir class ConversionError(Exception): @@ -169,3 +169,42 @@ def calibre_cover(title, author_string, series_string=None, lines.append(TextLine(series_string, author_size)) return create_cover_page(lines, I('library.png'), output_format='jpg') +UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$') + +def unit_convert(value, base, font, dpi): + ' Return value in pts' + if isinstance(value, (int, long, float)): + return value + try: + return float(value) * 72.0 / dpi + except: + pass + result = value + m = UNIT_RE.match(value) + if m is not None and m.group(1): + value = float(m.group(1)) + unit = m.group(2) + if unit == '%': + result = (value / 100.0) * base + elif unit == 'px': + result = value * 72.0 / dpi + elif unit == 'in': + result = value * 72.0 + elif unit == 'pt': + result = value + elif unit == 'em': + result = value * font + elif unit in ('ex', 'en'): + # This is a hack for ex since we have no way to know + # the x-height of the font + font = font + result = value * font * 0.5 + elif unit == 'pc': + result = value * 12.0 + elif unit == 'mm': + result = value * 0.04 + elif unit == 'cm': + result = value * 0.40 + return result + + diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 7c9a6bf48a..24814a34f9 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal ,' \ ' and Alex Bramley .' import os, re -from mimetypes import guess_type as guess_mimetype +from calibre import guess_type as guess_mimetype from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString from calibre.constants import iswindows, filesystem_encoding from calibre.utils.chm.chm import CHMFile diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index b26befe075..96ea3e5884 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -14,7 +14,8 @@ from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.date import parse_date from calibre.utils.zipfile import ZipFile -from calibre import extract, walk, isbytestring, filesystem_encoding +from calibre import (extract, walk, isbytestring, filesystem_encoding, + get_types_map) from calibre.constants import __version__ DEBUG_README=u''' @@ -875,6 +876,9 @@ OptionRecommendation(name='sr3_replace', if self.opts.verbose: self.log.filter_level = self.log.DEBUG self.flush() + import cssutils, logging + cssutils.log.setLevel(logging.WARN) + get_types_map() # Ensure the mimetypes module is intialized if self.opts.debug_pipeline is not None: self.opts.verbose = max(self.opts.verbose, 4) diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index a1d5fa94d8..8822a39b87 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -399,7 +399,7 @@ class HTMLPreProcessor(object): (re.compile(u'˙\s*()*\s*Z', re.UNICODE), lambda match: u'Ż'), # If pdf printed from a browser then the header/footer has a reliable pattern - (re.compile(r'((?<=)\s*file:////?[A-Z].*
|file:////?[A-Z].*
(?=\s*
))', re.IGNORECASE), lambda match: ''), + (re.compile(r'((?<=)\s*file:/{2,4}[A-Z].*
|file:////?[A-Z].*
(?=\s*
))', re.IGNORECASE), lambda match: ''), # Center separator lines (re.compile(u'
\s*(?P([*#•✦=]+\s*)+)\s*
'), lambda match: '

\n

' + match.group(1) + '

'), diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index f1f2f87293..1546644f95 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -764,6 +764,7 @@ class HeuristicProcessor(object): # Multiple sequential blank paragraphs are merged with appropriate margins # If non-blank scene breaks exist they are center aligned and styled with appropriate margins. if getattr(self.extra_opts, 'format_scene_breaks', False): + html = re.sub('(?i)]*>\s*\s*', '

', html) html = self.detect_whitespace(html) html = self.detect_soft_breaks(html) blanks_count = len(self.any_multi_blank.findall(html)) diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index 8d1164e026..b45f8f9f9e 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -10,7 +10,6 @@ Transform OEB content into FB2 markup from base64 import b64encode from datetime import datetime -from mimetypes import types_map import re import uuid @@ -18,9 +17,6 @@ from lxml import etree from calibre import prepare_string_for_xml from calibre.constants import __appname__, __version__ -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES, OPF from calibre.utils.magick import Image class FB2MLizer(object): @@ -71,7 +67,7 @@ class FB2MLizer(object): return u'' + output def clean_text(self, text): - # Condense empty paragraphs into a line break. + # Condense empty paragraphs into a line break. text = re.sub(r'(?miu)(

\s*

\s*){3,}', '', text) # Remove empty paragraphs. text = re.sub(r'(?miu)

\s*

', '', text) @@ -100,6 +96,7 @@ class FB2MLizer(object): return text def fb2_header(self): + from calibre.ebooks.oeb.base import OPF metadata = {} metadata['title'] = self.oeb_book.metadata.title[0].value metadata['appname'] = __appname__ @@ -180,6 +177,8 @@ class FB2MLizer(object): return u'' def get_cover(self): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + cover_href = None # Get the raster cover if it's available. @@ -213,6 +212,8 @@ class FB2MLizer(object): return u'' def get_text(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer text = [''] # Create main section if there are no others to create @@ -248,6 +249,8 @@ class FB2MLizer(object): ''' This function uses the self.image_hrefs dictionary mapping. It is populated by the dump_text function. ''' + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + images = [] for item in self.oeb_book.manifest: # Don't write the image if it's not referenced in the document's text. @@ -255,7 +258,7 @@ class FB2MLizer(object): continue if item.media_type in OEB_RASTER_IMAGES: try: - if not item.media_type == types_map['.jpeg'] or not item.media_type == types_map['.jpg']: + if item.media_type != 'image/jpeg': im = Image() im.load(item.data) im.set_compression_quality(70) @@ -344,6 +347,8 @@ class FB2MLizer(object): @return: List of string representing the XHTML converted to FB2 markup. ''' + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + # Ensure what we are converting is not a string and that the fist tag is part of the XHTML namespace. if not isinstance(elem_tree.tag, basestring) or namespace(elem_tree.tag) != XHTML_NS: return [] diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index dd0a247a67..079e990de3 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -315,7 +315,8 @@ class HTMLInput(InputFormatPlugin): from calibre import guess_type from calibre.ebooks.oeb.transforms.metadata import \ meta_info_to_oeb_metadata - import cssutils + import cssutils, logging + cssutils.log.setLevel(logging.WARN) self.OEB_STYLES = OEB_STYLES oeb = create_oebbook(log, None, opts, self, encoding=opts.input_encoding, populate=False) diff --git a/src/calibre/ebooks/html/meta.py b/src/calibre/ebooks/html/meta.py index 9a088efb16..07cf9236fc 100644 --- a/src/calibre/ebooks/html/meta.py +++ b/src/calibre/ebooks/html/meta.py @@ -4,7 +4,6 @@ __copyright__ = '2010, Fabian Grassl ' __docformat__ = 'restructuredtext en' -from calibre.ebooks.oeb.base import namespace, barename, DC11_NS class EasyMeta(object): @@ -12,6 +11,7 @@ class EasyMeta(object): self.meta = meta def __iter__(self): + from calibre.ebooks.oeb.base import namespace, barename, DC11_NS meta = self.meta for item_name in meta.items: for item in meta[item_name]: diff --git a/src/calibre/ebooks/html/output.py b/src/calibre/ebooks/html/output.py index 5c984162ac..fe7b4cf274 100644 --- a/src/calibre/ebooks/html/output.py +++ b/src/calibre/ebooks/html/output.py @@ -12,7 +12,6 @@ from os.path import dirname, abspath, relpath, exists, basename from lxml import etree from templite import Templite -from calibre.ebooks.oeb.base import element from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation from calibre import CurrentDir from calibre.ptempfile import PersistentTemporaryDirectory @@ -51,6 +50,7 @@ class HTMLOutput(OutputFormatPlugin): ''' Generate table of contents ''' + from calibre.ebooks.oeb.base import element with CurrentDir(output_dir): def build_node(current_node, parent=None): if parent is None: diff --git a/src/calibre/ebooks/htmlz/output.py b/src/calibre/ebooks/htmlz/output.py index 03fe12c89e..6d2ad54a12 100644 --- a/src/calibre/ebooks/htmlz/output.py +++ b/src/calibre/ebooks/htmlz/output.py @@ -12,7 +12,6 @@ from lxml import etree from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile @@ -42,6 +41,8 @@ class HTMLZOutput(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME + # HTML if opts.htmlz_css_type == 'inline': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer @@ -72,7 +73,7 @@ class HTMLZOutput(OutputFormatPlugin): for item in oeb_book.manifest: if item.media_type in OEB_IMAGES and item.href in images: if item.media_type == SVG_MIME: - data = unicode(etree.tostring(item.data, encoding=unicode)) + data = unicode(etree.tostring(item.data, encoding=unicode)) else: data = item.data fname = os.path.join(tdir, 'images', images[item.href]) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 6078a0aa94..2ae5f3ade5 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -6,11 +6,11 @@ __docformat__ = 'restructuredtext en' """ Provides abstraction for metadata reading.writing from a variety of ebook formats. """ -import os, mimetypes, sys, re +import os, sys, re from urllib import unquote, quote from urlparse import urlparse -from calibre import relpath +from calibre import relpath, guess_type from calibre.utils.config import tweaks @@ -118,7 +118,7 @@ class Resource(object): self.path = None self.fragment = '' try: - self.mime_type = mimetypes.guess_type(href_or_path)[0] + self.mime_type = guess_type(href_or_path)[0] except: self.mime_type = None if self.mime_type is None: diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 167ae52fa3..faac8e98b1 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -592,7 +592,7 @@ class Metadata(object): elif datatype == 'bool': res = _('Yes') if res else _('No') elif datatype == 'rating': - res = res/2 + res = res/2.0 return (name, unicode(res), orig_res, cmeta) # convert top-level ids into their value @@ -625,6 +625,8 @@ class Metadata(object): res = res + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) + elif datatype == 'rating': + res = res/2.0 return (name, unicode(res), orig_res, fmeta) return (None, None, None, None) diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py index 2d6192f949..21f15b05ae 100644 --- a/src/calibre/ebooks/metadata/fb2.py +++ b/src/calibre/ebooks/metadata/fb2.py @@ -5,11 +5,12 @@ __copyright__ = '2008, Anatoly Shipitsin ' '''Read meta information from fb2 files''' -import mimetypes, os +import os from base64 import b64decode from lxml import etree from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.chardet import xml_to_unicode +from calibre import guess_all_extensions XLINK_NS = 'http://www.w3.org/1999/xlink' def XLINK(name): @@ -71,7 +72,7 @@ def get_metadata(stream): binary = XPath('//fb2:binary[@id="%s"]'%id)(root) if binary: mt = binary[0].get('content-type', 'image/jpeg') - exts = mimetypes.guess_all_extensions(mt) + exts = guess_all_extensions(mt) if not exts: exts = ['.jpg'] cdata = (exts[0][1:], b64decode(tostring(binary[0]))) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index d360451e2e..58c887bfdb 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' lxml based OPF parser. ''' -import re, sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO, json +import re, sys, unittest, functools, os, uuid, glob, cStringIO, json from urllib import unquote from urlparse import urlparse @@ -20,7 +20,7 @@ from calibre.ebooks.metadata import string_to_authors, MetaInformation, check_is from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.date import parse_date, isoformat from calibre.utils.localization import get_lang -from calibre import prints +from calibre import prints, guess_type from calibre.utils.cleantext import clean_ascii_chars class Resource(object): # {{{ @@ -42,7 +42,7 @@ class Resource(object): # {{{ self.path = None self.fragment = '' try: - self.mime_type = mimetypes.guess_type(href_or_path)[0] + self.mime_type = guess_type(href_or_path)[0] except: self.mime_type = None if self.mime_type is None: @@ -1000,7 +1000,7 @@ class OPF(object): # {{{ for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): for item in self.guide: if item.type.lower() == t: - self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0]) + self.create_manifest_item(item.href(), guess_type(path)[0]) return property(fget=fget, fset=fset) diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index c9639bf531..86a9fe1133 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -274,26 +274,34 @@ class Source(Plugin): if authors: # Leave ' in there for Irish names - pat = re.compile(r'[-,:;+!@#$%^&*(){}.`~"\s\[\]/]') + remove_pat = re.compile(r'[,!@#$%^&*(){}`~"\s\[\]/]') + replace_pat = re.compile(r'[-+.:;]') if only_first_author: authors = authors[:1] for au in authors: + au = replace_pat.sub(' ', au) parts = au.split() if ',' in au: # au probably in ln, fn form parts = parts[1:] + parts[:1] for tok in parts: - tok = pat.sub('', tok).strip() + tok = remove_pat.sub('', tok).strip() if len(tok) > 2 and tok.lower() not in ('von', ): yield tok - def get_title_tokens(self, title): + def get_title_tokens(self, title, strip_joiners=True, strip_subtitle=False): ''' Take a title and return a list of tokens useful for an AND search query. - Excludes connectives and punctuation. + Excludes connectives(optionally) and punctuation. ''' if title: + # strip sub-titles + if strip_subtitle: + subtitle = re.compile(r'([\(\[\{].*?[\)\]\}]|[/:\\].*$)') + if len(subtitle.sub('', title)) > 1: + title = subtitle.sub('', title) + title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in [ # Remove things like: (2010) (Omnibus) etc. @@ -305,17 +313,20 @@ class Source(Plugin): (r'(\d+),(\d+)', r'\1\2'), # Remove hyphens only if they have whitespace before them (r'(\s-)', ' '), - # Remove single quotes - (r"'", ''), + # Remove single quotes not followed by 's' + (r"'(?!s)", ''), # Replace other special chars with a space (r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ') ]] + for pat, repl in title_patterns: title = pat.sub(repl, title) + tokens = title.split() for token in tokens: token = token.strip() - if token and token.lower() not in ('a', 'and', 'the'): + if token and (not strip_joiners or token.lower() not in ('a', + 'and', 'the', '&')): yield token def split_jobs(self, jobs, num): @@ -363,7 +374,12 @@ class Source(Plugin): def get_book_url(self, identifiers): ''' Return the URL for the book identified by identifiers at this source. - If no URL is found, return None. + This URL must be browseable to by a human using a browser. It is meant + to provide a clickable link for the user to easily visit the books page + at this source. + If no URL is found, return None. This method must be quick, and + consistent, so only implement it if it is possible to construct the URL + from a known scheme given identifiers. ''' return None diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 1fb1a74679..4d21a0c210 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -382,7 +382,11 @@ def identify(log, abort, # {{{ log(plog) log('\n'+'*'*80) + dummy = Metadata(_('Unknown')) for i, result in enumerate(presults): + for f in plugin.prefs['ignore_fields']: + if ':' not in f: + setattr(result, f, getattr(dummy, f)) result.relevance_in_source = i result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable and plugin.get_cached_cover_url(result.identifiers) is not @@ -433,7 +437,7 @@ def urls_from_identifiers(identifiers): # {{{ pass isbn = identifiers.get('isbn', None) if isbn: - ans.append(('ISBN', + ans.append((isbn, 'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn)) return ans # }}} @@ -444,13 +448,18 @@ if __name__ == '__main__': # tests {{{ from calibre.ebooks.metadata.sources.test import (test_identify, title_test, authors_test) tests = [ + ( + {'title':'Magykal Papers', + 'authors':['Sage']}, + [title_test('The Magykal Papers', exact=True)], + ), + ( # An e-book ISBN not on Amazon, one of the authors is # unknown to Amazon {'identifiers':{'isbn': '9780307459671'}, 'title':'Invisible Gorilla', 'authors':['Christopher Chabris']}, - [title_test('The Invisible Gorilla', - exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])] + [title_test('The Invisible Gorilla', exact=True)] ), diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py new file mode 100755 index 0000000000..831dd473b3 --- /dev/null +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Fetch metadata using Overdrive Content Reserve +''' +import re, random, mechanize, copy, json +from threading import RLock +from Queue import Queue, Empty + +from lxml import html +from lxml.html import soupparser + +from calibre.ebooks.metadata import check_isbn +from calibre.ebooks.metadata.sources.base import Source +from calibre.ebooks.metadata.book.base import Metadata +from calibre.ebooks.chardet import xml_to_unicode +from calibre.library.comments import sanitize_comments_html + +ovrdrv_data_cache = {} +cache_lock = RLock() +base_url = 'http://search.overdrive.com/' + + +class OverDrive(Source): + + name = 'Overdrive' + description = _('Downloads metadata from Overdrive\'s Content Reserve') + + capabilities = frozenset(['identify', 'cover']) + touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate', + 'comments', 'publisher', 'identifier:isbn', 'series', 'series_index', + 'language', 'identifier:overdrive']) + has_html_comments = True + supports_gzip_transfer_encoding = False + cached_cover_url_is_reliable = True + + def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ + identifiers={}, timeout=30): + ovrdrv_id = identifiers.get('overdrive', None) + isbn = identifiers.get('isbn', None) + + br = self.browser + ovrdrv_data = self.to_ovrdrv_data(br, title, authors, ovrdrv_id) + if ovrdrv_data: + title = ovrdrv_data[8] + authors = ovrdrv_data[6] + mi = Metadata(title, authors) + self.parse_search_results(ovrdrv_data, mi) + if ovrdrv_id is None: + ovrdrv_id = ovrdrv_data[7] + if isbn is not None: + self.cache_isbn_to_identifier(isbn, ovrdrv_id) + + self.get_book_detail(br, ovrdrv_data[1], mi, ovrdrv_id, log) + + result_queue.put(mi) + + return None + # }}} + + def download_cover(self, log, result_queue, abort, # {{{ + title=None, authors=None, identifiers={}, timeout=30): + cached_url = self.get_cached_cover_url(identifiers) + if cached_url is None: + log.info('No cached cover found, running identify') + rq = Queue() + self.identify(log, rq, abort, title=title, authors=authors, + identifiers=identifiers) + if abort.is_set(): + return + results = [] + while True: + try: + results.append(rq.get_nowait()) + except Empty: + break + results.sort(key=self.identify_results_keygen( + title=title, authors=authors, identifiers=identifiers)) + for mi in results: + cached_url = self.get_cached_cover_url(mi.identifiers) + if cached_url is not None: + break + if cached_url is None: + log.info('No cover found') + return + + if abort.is_set(): + return + + ovrdrv_id = identifiers.get('overdrive', None) + br = self.browser + req = mechanize.Request(cached_url) + if ovrdrv_id is not None: + referer = self.get_base_referer()+'ContentDetails-Cover.htm?ID='+ovrdrv_id + req.add_header('referer', referer) + req.add_header('referer', referer) + log('Downloading cover from:', cached_url) + try: + cdata = br.open_novisit(req, timeout=timeout).read() + result_queue.put((self, cdata)) + except: + log.exception('Failed to download cover from:', cached_url) + # }}} + + def get_cached_cover_url(self, identifiers): # {{{ + url = None + ovrdrv_id = identifiers.get('overdrive', None) + if ovrdrv_id is None: + isbn = identifiers.get('isbn', None) + if isbn is not None: + ovrdrv_id = self.cached_isbn_to_identifier(isbn) + if ovrdrv_id is not None: + url = self.cached_identifier_to_cover_url(ovrdrv_id) + + return url + # }}} + + def get_base_referer(self): # to be used for passing referrer headers to cover download + choices = [ + 'http://overdrive.chipublib.org/82DC601D-7DDE-4212-B43A-09D821935B01/10/375/en/', + 'http://emedia.clevnet.org/9D321DAD-EC0D-490D-BFD8-64AE2C96ECA8/10/241/en/', + 'http://singapore.lib.overdrive.com/F11D55BE-A917-4D63-8111-318E88B29740/10/382/en/', + 'http://ebooks.nypl.org/20E48048-A377-4520-BC43-F8729A42A424/10/257/en/', + 'http://spl.lib.overdrive.com/5875E082-4CB2-4689-9426-8509F354AFEF/10/335/en/' + ] + return choices[random.randint(0, len(choices)-1)] + + def format_results(self, reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid): + fix_slashes = re.compile(r'\\/') + thumbimage = fix_slashes.sub('/', thumbimage) + worldcatlink = fix_slashes.sub('/', worldcatlink) + cover_url = re.sub('(?P(Ima?g(eType-)?))200', '\g100', thumbimage) + social_metadata_url = base_url+'TitleInfo.aspx?ReserveID='+reserveid+'&FormatID='+formatid + series_num = '' + if not series: + if subtitle: + title = od_title+': '+subtitle + else: + title = od_title + else: + title = od_title + m = re.search("([0-9]+$)", subtitle) + if m: + series_num = float(m.group(1)) + return [cover_url, social_metadata_url, worldcatlink, series, series_num, publisher, creators, reserveid, title] + + def safe_query(self, br, query_url, post=''): + ''' + The query must be initialized by loading an empty search results page + this page attempts to set a cookie that Mechanize doesn't like + copy the cookiejar to a separate instance and make a one-off request with the temp cookiejar + ''' + goodcookies = br._ua_handlers['_cookies'].cookiejar + clean_cj = mechanize.CookieJar() + cookies_to_copy = [] + for cookie in goodcookies: + copied_cookie = copy.deepcopy(cookie) + cookies_to_copy.append(copied_cookie) + for copied_cookie in cookies_to_copy: + clean_cj.set_cookie(copied_cookie) + + if post: + br.open_novisit(query_url, post) + else: + br.open_novisit(query_url) + + br.set_cookiejar(clean_cj) + + def overdrive_search(self, br, q, title, author): + # re-initialize the cookiejar to so that it's clean + clean_cj = mechanize.CookieJar() + br.set_cookiejar(clean_cj) + q_query = q+'default.aspx/SearchByKeyword' + q_init_search = q+'SearchResults.aspx' + # get first author as string - convert this to a proper cleanup function later + author_tokens = list(self.get_author_tokens(author, + only_first_author=True)) + title_tokens = list(self.get_title_tokens(title, + strip_joiners=False, strip_subtitle=True)) + + if len(title_tokens) >= len(author_tokens): + initial_q = ' '.join(title_tokens) + xref_q = '+'.join(author_tokens) + else: + initial_q = ' '.join(author_tokens) + xref_q = '+'.join(title_tokens) + + q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q + query = '{"szKeyword":"'+initial_q+'"}' + + # main query, requires specific Content Type header + req = mechanize.Request(q_query) + req.add_header('Content-Type', 'application/json; charset=utf-8') + br.open_novisit(req, query) + + # initiate the search without messing up the cookiejar + self.safe_query(br, q_init_search) + + # get the search results object + results = False + while results == False: + xreq = mechanize.Request(q_xref) + xreq.add_header('X-Requested-With', 'XMLHttpRequest') + xreq.add_header('Referer', q_init_search) + xreq.add_header('Accept', 'application/json, text/javascript, */*') + raw = br.open_novisit(xreq).read() + for m in re.finditer(ur'"iTotalDisplayRecords":(?P\d+).*?"iTotalRecords":(?P\d+)', raw): + if int(m.group('displayrecords')) >= 1: + results = True + elif int(m.group('totalrecords')) >= 1: + xref_q = '' + q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q + elif int(m.group('totalrecords')) == 0: + return '' + + return self.sort_ovrdrv_results(raw, title, title_tokens, author, author_tokens) + + + def sort_ovrdrv_results(self, raw, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None): + close_matches = [] + raw = re.sub('.*?\[\[(?P.*?)\]\].*', '[[\g]]', raw) + results = json.loads(raw) + #print results + # The search results are either from a keyword search or a multi-format list from a single ID, + # sort through the results for closest match/format + if results: + for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \ + thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \ + availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results: + #print "this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series + if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]: + #print "overdrive id is not None, searching based on format type priority" + return self.format_results(reserveid, od_title, subtitle, series, publisher, + creators, thumbimage, worldcatlink, formatid) + else: + creators = creators.split(', ') + # if an exact match in a preferred format occurs + if (author and creators[0] == author[0]) and od_title == title and int(formatid) in [1, 50, 410, 900] and thumbimage: + return self.format_results(reserveid, od_title, subtitle, series, publisher, + creators, thumbimage, worldcatlink, formatid) + else: + close_title_match = False + close_author_match = False + for token in title_tokens: + if od_title.lower().find(token.lower()) != -1: + close_title_match = True + else: + close_title_match = False + break + for author in creators: + for token in author_tokens: + if author.lower().find(token.lower()) != -1: + close_author_match = True + else: + close_author_match = False + break + if close_author_match: + break + if close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900] and thumbimage: + if subtitle and series: + close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) + else: + close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) + if close_matches: + return close_matches[0] + else: + return '' + else: + return '' + + def overdrive_get_record(self, br, q, ovrdrv_id): + search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}' + results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc' + + # re-initialize the cookiejar to so that it's clean + clean_cj = mechanize.CookieJar() + br.set_cookiejar(clean_cj) + # get the base url to set the proper session cookie + br.open_novisit(q) + + # initialize the search + self.safe_query(br, search_url) + + # get the results + req = mechanize.Request(results_url) + req.add_header('X-Requested-With', 'XMLHttpRequest') + req.add_header('Referer', search_url) + req.add_header('Accept', 'application/json, text/javascript, */*') + raw = br.open_novisit(req) + raw = str(list(raw)) + clean_cj = mechanize.CookieJar() + br.set_cookiejar(clean_cj) + return self.sort_ovrdrv_results(raw, None, None, None, ovrdrv_id) + + + def find_ovrdrv_data(self, br, title, author, isbn, ovrdrv_id=None): + q = base_url + if ovrdrv_id is None: + return self.overdrive_search(br, q, title, author) + else: + return self.overdrive_get_record(br, q, ovrdrv_id) + + + + def to_ovrdrv_data(self, br, title=None, author=None, ovrdrv_id=None): + ''' + Takes either a title/author combo or an Overdrive ID. One of these + two must be passed to this function. + ''' + if ovrdrv_id is not None: + with cache_lock: + ans = ovrdrv_data_cache.get(ovrdrv_id, None) + if ans: + return ans + elif ans is False: + return None + else: + ovrdrv_data = self.find_ovrdrv_data(br, title, author, ovrdrv_id) + else: + try: + ovrdrv_data = self.find_ovrdrv_data(br, title, author, ovrdrv_id) + except: + import traceback + traceback.print_exc() + ovrdrv_data = None + with cache_lock: + ovrdrv_data_cache[ovrdrv_id] = ovrdrv_data if ovrdrv_data else False + + return ovrdrv_data if ovrdrv_data else False + + + def parse_search_results(self, ovrdrv_data, mi): + ''' + Parse the formatted search results from the initial Overdrive query and + add the values to the metadta. + + The list object has these values: + [cover_url[0], social_metadata_url[1], worldcatlink[2], series[3], series_num[4], + publisher[5], creators[6], reserveid[7], title[8]] + + ''' + ovrdrv_id = ovrdrv_data[7] + mi.set_identifier('overdrive', ovrdrv_id) + + if len(ovrdrv_data[3]) > 1: + mi.series = ovrdrv_data[3] + if ovrdrv_data[4]: + try: + mi.series_index = float(ovrdrv_data[4]) + except: + pass + mi.publisher = ovrdrv_data[5] + mi.authors = ovrdrv_data[6] + mi.title = ovrdrv_data[8] + cover_url = ovrdrv_data[0] + if cover_url: + self.cache_identifier_to_cover_url(ovrdrv_id, + cover_url) + + + def get_book_detail(self, br, metadata_url, mi, ovrdrv_id, log): + try: + raw = br.open_novisit(metadata_url).read() + except Exception, e: + if callable(getattr(e, 'getcode', None)) and \ + e.getcode() == 404: + return False + raise + raw = xml_to_unicode(raw, strip_encoding_pats=True, + resolve_entities=True)[0] + try: + root = soupparser.fromstring(raw) + except: + return False + + pub_date = root.xpath("//div/label[@id='ctl00_ContentPlaceHolder1_lblPubDate']/text()") + lang = root.xpath("//div/label[@id='ctl00_ContentPlaceHolder1_lblLanguage']/text()") + subjects = root.xpath("//div/label[@id='ctl00_ContentPlaceHolder1_lblSubjects']/text()") + ebook_isbn = root.xpath("//td/label[@id='ctl00_ContentPlaceHolder1_lblIdentifier']/text()") + desc = root.xpath("//div/label[@id='ctl00_ContentPlaceHolder1_lblDescription']/ancestor::div[1]") + + if pub_date: + from calibre.utils.date import parse_date + try: + mi.pubdate = parse_date(pub_date[0].strip()) + except: + pass + if lang: + lang = lang[0].strip().lower() + mi.language = {'english':'en', 'french':'fr', 'german':'de', + 'spanish':'es'}.get(lang, None) + + if ebook_isbn: + #print "ebook isbn is "+str(ebook_isbn[0]) + isbn = check_isbn(ebook_isbn[0].strip()) + if isbn: + self.cache_isbn_to_identifier(isbn, ovrdrv_id) + mi.isbn = isbn + if subjects: + mi.tags = [tag.strip() for tag in subjects[0].split(',')] + + if desc: + desc = desc[0] + desc = html.tostring(desc, method='html', encoding=unicode).strip() + # remove all attributes from tags + desc = re.sub(r'<([a-zA-Z0-9]+)\s[^>]+>', r'<\1>', desc) + # Remove comments + desc = re.sub(r'(?s)', '', desc) + mi.comments = sanitize_comments_html(desc) + + return None + + +if __name__ == '__main__': + # To run these test use: + # calibre-debug -e src/calibre/ebooks/metadata/sources/overdrive.py + from calibre.ebooks.metadata.sources.test import (test_identify_plugin, + title_test, authors_test) + test_identify_plugin(OverDrive.name, + [ + + ( + {'title':'Foundation and Earth', + 'authors':['Asimov']}, + [title_test('Foundation and Earth', exact=True), + authors_test(['Isaac Asimov'])] + ), + + ( + {'title': 'Elephants', 'authors':['Agatha']}, + [title_test('Elephants Can Remember', exact=False), + authors_test(['Agatha Christie'])] + ), + ]) + diff --git a/src/calibre/ebooks/metadata/sources/test.py b/src/calibre/ebooks/metadata/sources/test.py index e280b0c038..c55f963003 100644 --- a/src/calibre/ebooks/metadata/sources/test.py +++ b/src/calibre/ebooks/metadata/sources/test.py @@ -67,6 +67,23 @@ def authors_test(authors): return test +def series_test(series, series_index): + series = series.lower() + + def test(mi): + ms = mi.series.lower() if mi.series else '' + if (ms == series) and (series_index == mi.series_index): + return True + if mi.series: + prints('Series test failed. Expected: \'%s [%d]\' found \'%s[%d]\''% \ + (series, series_index, ms, mi.series_index)) + else: + prints('Series test failed. Expected: \'%s [%d]\' found no series'% \ + (series, series_index)) + return False + + return test + def init_test(tdir_name): tdir = tempfile.gettempdir() lf = os.path.join(tdir, tdir_name.replace(' ', '')+'_identify_test.txt') diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index a65649dfd2..d9c6853795 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -20,7 +20,7 @@ from calibre.utils.filenames import ascii_filename from calibre.utils.date import parse_date from calibre.utils.cleantext import clean_ascii_chars from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks import DRMError +from calibre.ebooks import DRMError, unit_convert from calibre.ebooks.chardet import ENCODING_PATS from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi.huffcdic import HuffReader @@ -258,6 +258,8 @@ class MobiReader(object): } ''') self.tag_css_rules = {} + self.left_margins = {} + self.text_indents = {} if hasattr(filename_or_stream, 'read'): stream = filename_or_stream @@ -567,9 +569,21 @@ class MobiReader(object): elif tag.tag == 'img': tag.set('width', width) else: - styles.append('text-indent: %s' % self.ensure_unit(width)) + ewidth = self.ensure_unit(width) + styles.append('text-indent: %s' % ewidth) + try: + ewidth_val = unit_convert(ewidth, 12, 500, 166) + self.text_indents[tag] = ewidth_val + except: + pass if width.startswith('-'): styles.append('margin-left: %s' % self.ensure_unit(width[1:])) + try: + ewidth_val = unit_convert(ewidth[1:], 12, 500, 166) + self.left_margins[tag] = ewidth_val + except: + pass + if attrib.has_key('align'): align = attrib.pop('align').strip() if align: @@ -661,6 +675,26 @@ class MobiReader(object): if hasattr(parent, 'remove'): parent.remove(tag) + def get_left_whitespace(self, tag): + + def whitespace(tag): + lm = ti = 0.0 + if tag.tag == 'p': + ti = unit_convert('1.5em', 12, 500, 166) + if tag.tag == 'blockquote': + lm = unit_convert('2em', 12, 500, 166) + lm = self.left_margins.get(tag, lm) + ti = self.text_indents.get(tag, ti) + return lm + ti + + parent = tag + ans = 0.0 + while parent is not None: + ans += whitespace(parent) + parent = parent.getparent() + + return ans + def create_opf(self, htmlfile, guide=None, root=None): mi = getattr(self.book_header.exth, 'mi', self.embedded_mi) if mi is None: @@ -731,16 +765,45 @@ class MobiReader(object): except: text = '' text = ent_pat.sub(entity_to_unicode, text) - tocobj.add_item(toc.partition('#')[0], href[1:], + item = tocobj.add_item(toc.partition('#')[0], href[1:], text) + item.left_space = int(self.get_left_whitespace(x)) found = True if reached and found and x.get('class', None) == 'mbp_pagebreak': break if tocobj is not None: + tocobj = self.structure_toc(tocobj) opf.set_toc(tocobj) return opf, ncx_manifest_entry + def structure_toc(self, toc): + indent_vals = set() + for item in toc: + indent_vals.add(item.left_space) + if len(indent_vals) > 6 or len(indent_vals) < 2: + # Too many or too few levels, give up + return toc + indent_vals = sorted(indent_vals) + + last_found = [None for i in indent_vals] + + newtoc = TOC() + + def find_parent(level): + candidates = last_found[:level] + for x in reversed(candidates): + if x is not None: + return x + return newtoc + + for item in toc: + level = indent_vals.index(item.left_space) + parent = find_parent(level) + last_found[level] = parent.add_item(item.href, item.fragment, + item.text) + + return newtoc def sizeof_trailing_entries(self, data): def sizeof_trailing_entry(ptr, psize): diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 58083f807f..f2c9696976 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -8,23 +8,18 @@ __copyright__ = '2008, Marshall T. Vandegrift ' __docformat__ = 'restructuredtext en' import os, re, uuid, logging -from mimetypes import types_map from collections import defaultdict from itertools import count from urlparse import urldefrag, urlparse, urlunparse, urljoin from urllib import unquote as urlunquote from lxml import etree, html -from cssutils import CSSParser, parseString, parseStyle, replaceUrls -from cssutils.css import CSSRule - -import calibre -from calibre.constants import filesystem_encoding +from calibre.constants import filesystem_encoding, __version__ from calibre.translations.dynamic import translate from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.conversion.preprocess import CSSPreProcessor -from calibre import isbytestring, as_unicode +from calibre import isbytestring, as_unicode, get_types_map RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True) @@ -179,6 +174,9 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): If the ``link_repl_func`` returns None, the attribute or tag text will be removed completely. ''' + from cssutils import parseString, parseStyle, replaceUrls, log + log.setLevel(logging.WARN) + if resolve_base_href: resolve_base_href(root) for el, attrib, link, pos in iterlinks(root, find_links_in_css=False): @@ -248,7 +246,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): el.attrib['style'] = repl - +types_map = get_types_map() EPUB_MIME = types_map['.epub'] XHTML_MIME = types_map['.xhtml'] CSS_MIME = types_map['.css'] @@ -1075,7 +1073,9 @@ class Manifest(object): def _parse_css(self, data): - + from cssutils.css import CSSRule + from cssutils import CSSParser, log + log.setLevel(logging.WARN) def get_style_rules_from_import(import_rule): ans = [] if not import_rule.styleSheet: @@ -2011,7 +2011,7 @@ class OEBBook(object): name='dtb:uid', content=unicode(self.uid)) etree.SubElement(head, NCX('meta'), name='dtb:depth', content=str(self.toc.depth())) - generator = ''.join(['calibre (', calibre.__version__, ')']) + generator = ''.join(['calibre (', __version__, ')']) etree.SubElement(head, NCX('meta'), name='dtb:generator', content=generator) etree.SubElement(head, NCX('meta'), diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index ebc2f30d00..6c10436038 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -10,11 +10,9 @@ import sys, os, uuid, copy, re, cStringIO from itertools import izip from urlparse import urldefrag, urlparse from urllib import unquote as urlunquote -from mimetypes import guess_type from collections import defaultdict from lxml import etree -import cssutils from calibre.ebooks.oeb.base import OPF1_NS, OPF2_NS, OPF2_NSMAP, DC11_NS, \ DC_NSES, OPF, xml2text @@ -30,6 +28,7 @@ from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.utils.localization import get_lang from calibre.ptempfile import TemporaryDirectory from calibre.constants import __appname__, __version__ +from calibre import guess_type __all__ = ['OEBReader'] @@ -172,6 +171,7 @@ class OEBReader(object): return bad def _manifest_add_missing(self, invalid): + import cssutils manifest = self.oeb.manifest known = set(manifest.hrefs) unchecked = set(manifest.values()) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 634f7f5fce..4f06efba9f 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -12,17 +12,19 @@ import os, itertools, re, logging, copy, unicodedata from weakref import WeakKeyDictionary from xml.dom import SyntaxErr as CSSSyntaxError import cssutils -from cssutils.css import CSSStyleRule, CSSPageRule, CSSStyleDeclaration, \ - CSSValueList, CSSFontFaceRule, cssproperties +from cssutils.css import (CSSStyleRule, CSSPageRule, CSSStyleDeclaration, + CSSValueList, CSSFontFaceRule, cssproperties) from cssutils import profile as cssprofiles from lxml import etree from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError - from calibre import force_unicode +from calibre.ebooks import unit_convert from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize from calibre.ebooks.oeb.profile import PROFILES +cssutils.log.setLevel(logging.WARN) + _html_css_stylesheet = None def html_css_stylesheet(): @@ -443,7 +445,6 @@ class Stylizer(object): class Style(object): - UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$') MS_PAT = re.compile(r'^\s*(mso-|panose-|text-underline|tab-interval)') def __init__(self, element, stylizer): @@ -506,43 +507,11 @@ class Style(object): return result def _unit_convert(self, value, base=None, font=None): - ' Return value in pts' - if isinstance(value, (int, long, float)): - return value - try: - return float(value) * 72.0 / self._profile.dpi - except: - pass - result = value - m = self.UNIT_RE.match(value) - if m is not None and m.group(1): - value = float(m.group(1)) - unit = m.group(2) - if unit == '%': - if base is None: - base = self.width - result = (value / 100.0) * base - elif unit == 'px': - result = value * 72.0 / self._profile.dpi - elif unit == 'in': - result = value * 72.0 - elif unit == 'pt': - result = value - elif unit == 'em': - font = font or self.fontSize - result = value * font - elif unit in ('ex', 'en'): - # This is a hack for ex since we have no way to know - # the x-height of the font - font = font or self.fontSize - result = value * font * 0.5 - elif unit == 'pc': - result = value * 12.0 - elif unit == 'mm': - result = value * 0.04 - elif unit == 'cm': - result = value * 0.40 - return result + 'Return value in pts' + if base is None: + base = self.width + font = font or self.fontSize + return unit_convert(value, base, font, self._profile.dpi) def pt_to_px(self, value): return (self._profile.dpi / 72.0) * value diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index bad75b9a6f..c3c7f091c3 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -9,7 +9,6 @@ import posixpath from urlparse import urldefrag, urlparse from lxml import etree -import cssutils from calibre.ebooks.oeb.base import rewrite_links, urlnormalize @@ -25,6 +24,7 @@ class RenameFiles(object): # {{{ self.renamed_items_map = renamed_items_map def __call__(self, oeb, opts): + import cssutils self.log = oeb.logger self.opts = opts self.oeb = oeb diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index 0baacfd1f9..95501dbb9b 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -8,8 +8,6 @@ __copyright__ = '2008, Marshall T. Vandegrift ' from urlparse import urldefrag -import cssutils - from calibre.ebooks.oeb.base import CSS_MIME, OEB_DOCS from calibre.ebooks.oeb.base import urlnormalize, iterlinks @@ -23,6 +21,7 @@ class ManifestTrimmer(object): return cls() def __call__(self, oeb, context): + import cssutils oeb.logger.info('Trimming unused files from manifest...') self.opts = context used = set() diff --git a/src/calibre/ebooks/pdb/ereader/writer.py b/src/calibre/ebooks/pdb/ereader/writer.py index 4fbd343a6b..eb023c594b 100644 --- a/src/calibre/ebooks/pdb/ereader/writer.py +++ b/src/calibre/ebooks/pdb/ereader/writer.py @@ -21,7 +21,6 @@ except ImportError: import cStringIO from calibre.ebooks.pdb.formatwriter import FormatWriter -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.ebooks.pdb.header import PdbHeaderBuilder from calibre.ebooks.pml.pmlml import PMLMLizer @@ -135,6 +134,7 @@ class Writer(FormatWriter): 62-...: Raw image data in 8 bit PNG format. ''' images = [] + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES for item in manifest: if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): diff --git a/src/calibre/ebooks/pml/output.py b/src/calibre/ebooks/pml/output.py index 9d2ddc6ca6..63d8a8b220 100644 --- a/src/calibre/ebooks/pml/output.py +++ b/src/calibre/ebooks/pml/output.py @@ -18,7 +18,6 @@ from calibre.customize.conversion import OutputFormatPlugin from calibre.customize.conversion import OptionRecommendation from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.ebooks.pml.pmlml import PMLMLizer class PMLOutput(OutputFormatPlugin): @@ -60,6 +59,7 @@ class PMLOutput(OutputFormatPlugin): pmlz.add_dir(tdir) def write_images(self, manifest, image_hrefs, out_dir, opts): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES for item in manifest: if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): if opts.full_image_depth: diff --git a/src/calibre/ebooks/pml/pmlml.py b/src/calibre/ebooks/pml/pmlml.py index 779e75d713..b04aaacaec 100644 --- a/src/calibre/ebooks/pml/pmlml.py +++ b/src/calibre/ebooks/pml/pmlml.py @@ -12,8 +12,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.pdb.ereader import image_name from calibre.ebooks.pml import unipmlcode @@ -110,6 +108,9 @@ class PMLMLizer(object): return output def get_cover_page(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + output = u'' if 'cover' in self.oeb_book.guide: output += '\\m="cover.png"\n' @@ -125,6 +126,9 @@ class PMLMLizer(object): return output def get_text(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + text = [u''] for item in self.oeb_book.spine: self.log.debug('Converting %s to PML markup...' % item.href) @@ -180,7 +184,7 @@ class PMLMLizer(object): links = set(re.findall(r'(?<=\\q="#).+?(?=")', text)) for unused in anchors.difference(links): text = text.replace('\\Q="%s"' % unused, '') - + # Remove \Cn tags that are within \x and \Xn tags text = re.sub(ur'(?msu)(?P\\(x|X[0-4]))(?P.*?)(?P\\C[0-4]\s*=\s*"[^"]*")(?P.*?)(?P=t)', '\g\g\g\g', text) @@ -214,6 +218,8 @@ class PMLMLizer(object): return text def dump_text(self, elem, stylizer, page, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: return [] diff --git a/src/calibre/ebooks/rb/rbml.py b/src/calibre/ebooks/rb/rbml.py index 50153d7d4d..8cf63e334c 100644 --- a/src/calibre/ebooks/rb/rbml.py +++ b/src/calibre/ebooks/rb/rbml.py @@ -11,8 +11,6 @@ Transform OEB content into RB compatible markup. import re from calibre import prepare_string_for_xml -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.rb import unique_name TAGS = [ @@ -81,6 +79,8 @@ class RBMLizer(object): return output def get_cover_page(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML output = u'' if 'cover' in self.oeb_book.guide: if self.name_map.get(self.oeb_book.guide['cover'].href, None): @@ -109,6 +109,9 @@ class RBMLizer(object): return ''.join(toc) def get_text(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + output = [u''] for item in self.oeb_book.spine: self.log.debug('Converting %s to RocketBook HTML...' % item.href) @@ -137,6 +140,8 @@ class RBMLizer(object): return text def dump_text(self, elem, stylizer, page, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: return [u''] diff --git a/src/calibre/ebooks/rb/writer.py b/src/calibre/ebooks/rb/writer.py index c8908ee95f..f71b103fbd 100644 --- a/src/calibre/ebooks/rb/writer.py +++ b/src/calibre/ebooks/rb/writer.py @@ -18,7 +18,6 @@ import cStringIO from calibre.ebooks.rb.rbml import RBMLizer from calibre.ebooks.rb import HEADER from calibre.ebooks.rb import unique_name -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.constants import __appname__, __version__ TEXT_RECORD_SIZE = 4096 @@ -111,6 +110,7 @@ class RBWriter(object): return (size, pages) def _images(self, manifest): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES images = [] used_names = [] diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py index f739207018..97fa175d1a 100644 --- a/src/calibre/ebooks/rtf/rtfml.py +++ b/src/calibre/ebooks/rtf/rtfml.py @@ -14,9 +14,6 @@ import cStringIO from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace, \ - OEB_RASTER_IMAGES -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.metadata import authors_to_string from calibre.utils.filenames import ascii_text from calibre.utils.magick.draw import save_cover_data_to, identify_data @@ -100,6 +97,8 @@ class RTFMLizer(object): return self.mlize_spine() def mlize_spine(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = self.header() if 'titlepage' in self.oeb_book.guide: href = self.oeb_book.guide['titlepage'].href @@ -154,6 +153,8 @@ class RTFMLizer(object): return ' }' def insert_images(self, text): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + for item in self.oeb_book.manifest: if item.media_type in OEB_RASTER_IMAGES: src = os.path.basename(item.href) @@ -201,6 +202,8 @@ class RTFMLizer(object): return text def dump_text(self, elem, stylizer, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, namespace, barename + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: p = elem.getparent() diff --git a/src/calibre/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py index 100ac1447f..13b1ca45f9 100755 --- a/src/calibre/ebooks/snb/input.py +++ b/src/calibre/ebooks/snb/input.py @@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en' import os, uuid from calibre.customize.conversion import InputFormatPlugin -from calibre.ebooks.oeb.base import DirContainer from calibre.ebooks.snb.snbfile import SNBFile from calibre.ptempfile import TemporaryDirectory from calibre.utils.filenames import ascii_filename @@ -30,6 +29,7 @@ class SNBInput(InputFormatPlugin): def convert(self, stream, options, file_ext, log, accelerators): + from calibre.ebooks.oeb.base import DirContainer log.debug("Parsing SNB file...") snbFile = SNBFile() try: diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index 1a0986baf4..be4e537825 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -5,7 +5,8 @@ __copyright__ = '2010, Li Fanxi ' __docformat__ = 'restructuredtext en' import sys, struct, zlib, bz2, os -from mimetypes import types_map + +from calibre import guess_type class FileStream: def IsBinary(self): @@ -180,7 +181,7 @@ class SNBFile: file = open(os.path.join(path, fname), 'wb') file.write(f.fileBody) file.close() - fileNames.append((fname, types_map[ext])) + fileNames.append((fname, guess_type('a'+ext)[0])) return fileNames def Output(self, outputFile): diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 078e7ebe76..a501de1ff0 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -13,8 +13,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer def ProcessFileName(fileName): # Flat the path @@ -81,6 +79,8 @@ class SNBMLizer(object): body.append(entity) def mlize(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = [ u'' ] stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile) content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode)) @@ -208,6 +208,7 @@ class SNBMLizer(object): return text def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: diff --git a/src/calibre/ebooks/txt/output.py b/src/calibre/ebooks/txt/output.py index 4e54a97b45..ac63690996 100644 --- a/src/calibre/ebooks/txt/output.py +++ b/src/calibre/ebooks/txt/output.py @@ -11,7 +11,6 @@ from lxml import etree from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.ebooks.txt.txtml import TXTMLizer from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines from calibre.ptempfile import TemporaryDirectory, TemporaryFile @@ -103,12 +102,13 @@ class TXTOutput(OutputFormatPlugin): class TXTZOutput(TXTOutput): - + name = 'TXTZ Output' author = 'John Schember' file_type = 'txtz' def convert(self, oeb_book, output_path, input_plugin, opts, log): + from calibre.ebooks.oeb.base import OEB_IMAGES with TemporaryDirectory('_txtz_output') as tdir: # TXT with TemporaryFile('index.txt') as tf: @@ -123,10 +123,10 @@ class TXTZOutput(TXTOutput): os.makedirs(path) with open(os.path.join(tdir, item.href), 'wb') as imgf: imgf.write(item.data) - + # Metadata - with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: + with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: mdataf.write(etree.tostring(oeb_book.metadata.to_opf1())) - + txtz = ZipFile(output_path, 'w') txtz.add_dir(tdir) diff --git a/src/calibre/ebooks/txt/txtml.py b/src/calibre/ebooks/txt/txtml.py index fa7bfbb380..2320fbbbc7 100644 --- a/src/calibre/ebooks/txt/txtml.py +++ b/src/calibre/ebooks/txt/txtml.py @@ -12,8 +12,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer BLOCK_TAGS = [ 'div', @@ -58,12 +56,14 @@ class TXTMLizer(object): self.toc_titles = [] self.toc_ids = [] self.last_was_heading = False - + self.create_flat_toc(self.oeb_book.toc) return self.mlize_spine() def mlize_spine(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = [u''] output.append(self.get_toc()) for item in self.oeb_book.spine: @@ -139,7 +139,7 @@ class TXTMLizer(object): # when remove paragraph spacing is enabled. text = re.sub('(?imu)^[ ]+', '', text) text = re.sub('(?imu)[ ]+$', '', text) - + # Remove empty space and newlines at the beginning of the document. text = re.sub(r'(?u)^[ \n]+', '', text) @@ -185,6 +185,7 @@ class TXTMLizer(object): @stylizer: The style information attached to the element. @page: OEB page used to determine absolute urls. ''' + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 773aea3002..de066359ed 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -4,19 +4,17 @@ __copyright__ = '2008, Kovid Goyal ' import os, sys, Queue, threading from threading import RLock from urllib import unquote - -from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ - QByteArray, QTranslator, QCoreApplication, QThread, \ - QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ - QFileDialog, QFileIconProvider, \ - QIcon, QApplication, QDialog, QUrl, QFont +from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, + QByteArray, QTranslator, QCoreApplication, QThread, + QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, + QFileDialog, QFileIconProvider, + QIcon, QApplication, QDialog, QUrl, QFont) ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.localization import set_qt_translator -from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.ebooks.metadata import MetaInformation from calibre.utils.date import UNDEFINED_DATE @@ -156,7 +154,9 @@ def _config(): c.add_opt('plugin_search_history', default=[], help='Search history for the recipe scheduler') c.add_opt('worker_limit', default=6, - help=_('Maximum number of waiting worker processes')) + help=_( + 'Maximum number of simultaneous conversion/news download jobs. ' + 'This number is twice the actual value for historical reasons.')) c.add_opt('get_social_metadata', default=True, help=_('Download social metadata (tags/rating/etc.)')) c.add_opt('overwrite_author_title_metadata', default=True, @@ -330,6 +330,7 @@ class GetMetadata(QObject): id, args, kwargs) def _from_formats(self, id, args, kwargs): + from calibre.ebooks.metadata.meta import metadata_from_formats try: mi = metadata_from_formats(*args, **kwargs) except: @@ -337,6 +338,7 @@ class GetMetadata(QObject): self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi) def _get_metadata(self, id, args, kwargs): + from calibre.ebooks.metadata.meta import get_metadata try: mi = get_metadata(*args, **kwargs) except: @@ -738,3 +740,4 @@ def build_forms(srcdir, info=None): _df = os.environ.get('CALIBRE_DEVELOP_FROM', None) if _df and os.path.exists(_df): build_forms(_df) + diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index 48397936fb..f934a4a53c 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -22,7 +22,7 @@ class FetchAnnotationsAction(InterfaceAction): action_type = 'current' def genesis(self): - pass + self.qaction.triggered.connect(self.fetch_annotations) def fetch_annotations(self, *args): # Generate a path_map from selected ids @@ -52,6 +52,10 @@ class FetchAnnotationsAction(InterfaceAction): return path_map device = self.gui.device_manager.device + if not getattr(device, 'SUPPORTS_ANNOTATIONS', False): + return error_dialog(self.gui, _('Not supported'), + _('Fetching annotations is not supported for this device'), + show=True) if self.gui.current_view() is not self.gui.library_view: return error_dialog(self.gui, _('Use library only'), diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 18a73fb282..9d4d3891ca 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -37,8 +37,6 @@ class EditMetadataAction(InterfaceAction): md.addSeparator() if test_eight_code: dall = self.download_metadata - dident = partial(self.download_metadata, covers=False) - dcovers = partial(self.download_metadata, identify=False) else: dall = partial(self.download_metadata_old, False, covers=True) dident = partial(self.download_metadata_old, False, covers=False) @@ -47,9 +45,9 @@ class EditMetadataAction(InterfaceAction): md.addAction(_('Download metadata and covers'), dall, Qt.ControlModifier+Qt.Key_D) - md.addAction(_('Download only metadata'), dident) - md.addAction(_('Download only covers'), dcovers) if not test_eight_code: + md.addAction(_('Download only metadata'), dident) + md.addAction(_('Download only covers'), dcovers) md.addAction(_('Download only social metadata'), partial(self.download_metadata_old, False, covers=False, set_metadata=False, set_social_metadata=True)) @@ -80,7 +78,7 @@ class EditMetadataAction(InterfaceAction): self.qaction.setEnabled(enabled) self.action_merge.setEnabled(enabled) - def download_metadata(self, identify=True, covers=True, ids=None): + def download_metadata(self, ids=None): if ids is None: rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: @@ -90,7 +88,7 @@ class EditMetadataAction(InterfaceAction): ids = [db.id(row.row()) for row in rows] from calibre.gui2.metadata.bulk_download2 import start_download start_download(self.gui, ids, - Dispatcher(self.bulk_metadata_downloaded), identify, covers) + Dispatcher(self.bulk_metadata_downloaded)) def bulk_metadata_downloaded(self, job): if job.failed: diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index 5ec3df5c10..1c232f8483 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -47,16 +47,21 @@ class StoreAction(InterfaceAction): if self.config.get('first_run', True): self.config['first_run'] = False from calibre.gui2 import info_dialog - info_dialog(self.gui, _('Get Books Disclaimer'), - _('

Calibre helps you find books to read by connecting you with outside stores. ' - 'The stores are a variety of big, independent, free, and public domain sources.

' - '

Using the integrated search you can easily find what store has the book you\'re ' - 'looking for. It will also give you a price, DRM status as well as a lot of ' - 'other useful information.

' - '
'), - show=True, show_copy_button=False) + info_dialog(self.gui, _('About Get Books'), '

' + + _('Calibre helps you find the ebooks you want by searching ' + 'the websites of a variety of commercial and public domain ' + 'book sources for you.') + + '

' + + _('Using the integrated search you can easily find which ' + 'store has the book you are looking for, at the best price. ' + 'You will also get DRM status and other useful information.') + + '

' + + _('All transactions (paid or otherwise) are handled between ' + 'you and the particular website. ' + 'Calibre is not part of this process and any issues related ' + 'to a purchase should be directed to the website you are ' + 'buying from. Be sure to double check that any books you get ' + 'will work with your e-book reader, especially if the book you ' + 'are buying has ' + 'DRM.' + ), show=True, show_copy_button=False) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 6c3dae3c94..4e75a42e89 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -418,6 +418,7 @@ class BookDetails(QWidget): # {{{ if y is None: # Local image self.cover_view.paste_from_clipboard(x) + self.update_layout() else: self.remote_file_dropped.emit(x, y) # We do not support setting cover *and* adding formats for @@ -449,6 +450,7 @@ class BookDetails(QWidget): # {{{ self.setAcceptDrops(True) self._layout = DetailsLayout(vertical, self) self.setLayout(self._layout) + self.current_path = '' self.cover_view = CoverView(vertical, self) self.cover_view.cover_changed.connect(self.cover_changed.emit) @@ -482,15 +484,19 @@ class BookDetails(QWidget): # {{{ def show_data(self, data): self.book_info.show_data(data) self.cover_view.show_data(data) + self.current_path = data.get(_('Path'), '') + self.update_layout() + + def update_layout(self): self._layout.do_layout(self.rect()) try: sz = self.cover_view.pixmap.size() except: sz = QSize(0, 0) self.setToolTip( - '

'+_('Double-click to open Book Details window') + - '

' + _('Path') + ': ' + data.get(_('Path'), '') + - '

' + _('Cover size: %dx%d')%(sz.width(), sz.height()) + '

'+_('Double-click to open Book Details window') + + '

' + _('Path') + ': ' + self.current_path + + '

' + _('Cover size: %dx%d')%(sz.width(), sz.height()) ) def reset_info(self): diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 81016d3c6a..d1acd2ed83 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -289,6 +289,7 @@ class Series(Base): values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(key=sort_key) w = MultiCompleteComboBox(parent) + w.set_separator(None) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setMinimumContentsLength(25) self.name_widget = w diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 4d4f66eab1..49542abdc1 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -7,7 +7,7 @@ import os, traceback, Queue, time, cStringIO, re, sys from threading import Thread from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \ - Qt, pyqtSignal, QDialog + Qt, pyqtSignal, QDialog, QObject from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins @@ -25,12 +25,10 @@ from calibre.devices.errors import FreeSpaceError from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi -from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import DEBUG from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import thumbnail -from calibre.library.save_to_disk import plugboard_any_device_value, \ - plugboard_any_format_value +from calibre.library.save_to_disk import find_plugboard # }}} class DeviceJob(BaseJob): # {{{ @@ -93,23 +91,6 @@ class DeviceJob(BaseJob): # {{{ # }}} -def find_plugboard(device_name, format, plugboards): - cpb = None - if format in plugboards: - cpb = plugboards[format] - elif plugboard_any_format_value in plugboards: - cpb = plugboards[plugboard_any_format_value] - if cpb is not None: - if device_name in cpb: - cpb = cpb[device_name] - elif plugboard_any_device_value in cpb: - cpb = cpb[plugboard_any_device_value] - else: - cpb = None - if DEBUG: - prints('Device using plugboard', format, device_name, cpb) - return cpb - def device_name_for_plugboards(device_class): if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'): return device_class.DEVICE_PLUGBOARD_NAME @@ -352,6 +333,7 @@ class DeviceManager(Thread): # {{{ def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None): '''Upload books to device: ''' + from calibre.ebooks.metadata.meta import set_metadata if hasattr(self.connected_device, 'set_plugboards') and \ callable(self.connected_device.set_plugboards): self.connected_device.set_plugboards(plugboards, find_plugboard) @@ -605,6 +587,24 @@ class DeviceMenu(QMenu): # {{{ # }}} +class DeviceSignals(QObject): + #: This signal is emitted once, after metadata is downloaded from the + #: connected device. + #: The sequence: gui.device_manager.is_device_connected will become True, + #: and the device_connection_changed signal will be emitted, + #: then sometime later gui.device_metadata_available will be signaled. + #: This does not mean that there are no more jobs running. Automatic metadata + #: management might have kicked off a sync_booklists to write new metadata onto + #: the device, and that job might still be running when the signal is emitted. + device_metadata_available = pyqtSignal() + + #: This signal is emitted once when the device is detected and once when + #: it is disconnected. If the parameter is True, then it is a connection, + #: otherwise a disconnection. + device_connection_changed = pyqtSignal(object) + +device_signals = DeviceSignals() + class DeviceMixin(object): # {{{ def __init__(self): @@ -753,6 +753,7 @@ class DeviceMixin(object): # {{{ self.location_manager.update_devices() self.library_view.set_device_connected(self.device_connected) self.refresh_ondevice() + device_signals.device_connection_changed.emit(connected) def info_read(self, job): ''' @@ -791,6 +792,7 @@ class DeviceMixin(object): # {{{ self.sync_news() self.sync_catalogs() self.refresh_ondevice() + device_signals.device_metadata_available.emit() def refresh_ondevice(self, reset_only = False): ''' @@ -892,7 +894,7 @@ class DeviceMixin(object): # {{{ sub_dest_parts.append('') to = sub_dest_parts[0] fmts = sub_dest_parts[1] - subject = ';'.join(sub_dest_parts[2:]) + subject = ';'.join(sub_dest_parts[2:]) fmts = [x.strip().lower() for x in fmts.split(',')] self.send_by_mail(to, fmts, delete, subject=subject) diff --git a/src/calibre/gui2/dialogs/confirm_delete.py b/src/calibre/gui2/dialogs/confirm_delete.py index 16d7bdde2f..fe4ad60ace 100644 --- a/src/calibre/gui2/dialogs/confirm_delete.py +++ b/src/calibre/gui2/dialogs/confirm_delete.py @@ -24,11 +24,16 @@ class Dialog(QDialog, Ui_Dialog): dynamic[confirm_config_name(self.name)] = self.again.isChecked() -def confirm(msg, name, parent=None, pixmap='dialog_warning.png'): +def confirm(msg, name, parent=None, pixmap='dialog_warning.png', title=None, + show_cancel_button=True): if not dynamic.get(confirm_config_name(name), True): return True d = Dialog(msg, name, parent) d.label.setPixmap(QPixmap(I(pixmap))) d.setWindowIcon(QIcon(I(pixmap))) + if title is not None: + d.setWindowTitle(title) + if not show_cancel_button: + d.buttonBox.button(d.buttonBox.Cancel).setVisible(False) d.resize(d.sizeHint()) return d.exec_() == d.Accepted diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 8a97183ffe..66cf55a9b2 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -13,7 +13,6 @@ from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.ebooks.metadata import string_to_authors, authors_to_string, title_sort from calibre.ebooks.metadata.book.base import composite_formatter -from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, \ gprefs, question_dialog @@ -26,6 +25,7 @@ from calibre.utils.magick.draw import identify_data from calibre.utils.date import qt_to_dt def get_cover_data(path): # {{{ + from calibre.ebooks.metadata.meta import get_metadata old = prefs['read_file_metadata'] if not old: prefs['read_file_metadata'] = True diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index f6b7b94453..4776562c29 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -25,7 +25,6 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata import string_to_authors, \ authors_to_string, check_isbn, title_sort from calibre.ebooks.metadata.covers import download_cover -from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata import MetaInformation from calibre.utils.config import prefs, tweaks from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp @@ -353,6 +352,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.formats_changed = True def get_selected_format_metadata(self): + from calibre.ebooks.metadata.meta import get_metadata old = prefs['read_file_metadata'] if not old: prefs['read_file_metadata'] = True diff --git a/src/calibre/gui2/dialogs/tweak_epub.py b/src/calibre/gui2/dialogs/tweak_epub.py index a42fb07e40..edc274c9b2 100755 --- a/src/calibre/gui2/dialogs/tweak_epub.py +++ b/src/calibre/gui2/dialogs/tweak_epub.py @@ -12,7 +12,7 @@ from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from PyQt4.Qt import QDialog -from calibre.constants import isosx, iswindows +from calibre.constants import isosx from calibre.gui2 import open_local_file from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog from calibre.libunzip import extract as zipextract diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c72b074463..7250103615 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -156,8 +156,6 @@ class SearchBar(QWidget): # {{{ x = ComboBoxWithHelp(self) x.setMaximumSize(QSize(150, 16777215)) x.setObjectName("search_restriction") - x.setToolTip(_('Books display will be restricted to those matching the ' - 'selected saved search')) l.addWidget(x) parent.search_restriction = x diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 8d89ec76ed..0bd3f2133a 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -18,7 +18,6 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import tweaks, prefs from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.icu import sort_key -from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.utils.search_query_parser import SearchQueryParser from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ REGEXP_MATCH, MetadataBackup, force_to_bool @@ -478,6 +477,7 @@ class BooksModel(QAbstractTableModel): # {{{ def get_preferred_formats_from_ids(self, ids, formats, set_metadata=False, specific_format=None, exclude_auto=False, mode='r+b'): + from calibre.ebooks.metadata.meta import set_metadata as _set_metadata ans = [] need_auto = [] if specific_format is not None: @@ -526,6 +526,7 @@ class BooksModel(QAbstractTableModel): # {{{ def get_preferred_formats(self, rows, formats, paths=False, set_metadata=False, specific_format=None, exclude_auto=False): + from calibre.ebooks.metadata.meta import set_metadata as _set_metadata ans = [] need_auto = [] if specific_format is not None: diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index c67ec8c2b4..ee18d8e9ca 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -19,6 +19,9 @@ from calibre.utils.config import prefs, dynamic from calibre.library.database2 import LibraryDatabase2 from calibre.library.sqlite import sqlite, DatabaseException +if iswindows: + winutil = plugins['winutil'][0] + def option_parser(): parser = _option_parser('''\ %prog [opts] [path_to_ebook] @@ -80,8 +83,7 @@ def get_library_path(parent=None): if library_path is None: # Need to migrate to new database layout base = os.path.expanduser('~') if iswindows: - base = plugins['winutil'][0].special_folder_path( - plugins['winutil'][0].CSIDL_PERSONAL) + base = winutil.special_folder_path(winutil.CSIDL_PERSONAL) if not base or not os.path.exists(base): from PyQt4.Qt import QDir base = unicode(QDir.homePath()).replace('/', os.sep) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 9502fcb205..d34be6c564 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -24,7 +24,7 @@ from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ choose_files, error_dialog, choose_images, question_dialog from calibre.utils.date import local_tz, qt_to_dt -from calibre import strftime, fit_image +from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS from calibre.customize.ui import run_plugins_on_import from calibre.utils.date import utcfromtimestamp @@ -278,11 +278,13 @@ class AuthorSortEdit(EnLineEdit): def copy_to_authors(self): aus = self.current_val + meth = tweaks['author_sort_copy_method'] if aus: ln, _, rest = aus.partition(',') if rest: - au = rest.strip() + ' ' + ln.strip() - self.authors_edit.current_val = [au] + if meth in ('invert', 'nocomma', 'comma'): + aus = rest.strip() + ' ' + ln.strip() + self.authors_edit.current_val = [aus] def auto_generate(self, *args): au = unicode(self.authors_edit.text()) @@ -465,16 +467,22 @@ class FormatsManager(QWidget): # {{{ self.metadata_from_format_button = QToolButton(self) self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) self.metadata_from_format_button.setIconSize(QSize(32, 32)) + self.metadata_from_format_button.setToolTip( + _('Set metadata for the book from the selected format')) self.add_format_button = QToolButton(self) self.add_format_button.setIcon(QIcon(I('add_book.png'))) self.add_format_button.setIconSize(QSize(32, 32)) self.add_format_button.clicked.connect(self.add_format) + self.add_format_button.setToolTip( + _('Add a format to this book')) self.remove_format_button = QToolButton(self) self.remove_format_button.setIcon(QIcon(I('trash.png'))) self.remove_format_button.setIconSize(QSize(32, 32)) self.remove_format_button.clicked.connect(self.remove_format) + self.remove_format_button.setToolTip( + _('Remove the selected format from this book')) self.formats = FormatList(self) self.formats.setAcceptDrops(True) @@ -664,12 +672,7 @@ class Cover(ImageView): # {{{ self.frame_size = (sz.width()//3, sz.height()) def sizeHint(self): - sz = ImageView.sizeHint(self) - w, h = sz.width(), sz.height() - resized, nw, nh = fit_image(w, h, self.frame_size[0], - self.frame_size[1]) - if resized: - sz = QSize(nw, nh) + sz = QSize(self.frame_size[0], self.frame_size[1]) return sz def select_cover(self, *args): @@ -939,7 +942,13 @@ class IdentifiersEdit(QLineEdit): # {{{ def fset(self, val): if not val: val = {} - txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()]) + def keygen(x): + x = x[0] + if x == 'isbn': + x = '00isbn' + return x + ids = sorted(val.iteritems(), key=keygen) + txt = ', '.join(['%s:%s'%(k, v) for k, v in ids]) self.setText(txt.strip()) self.setCursorPosition(0) return property(fget=fget, fset=fset) @@ -959,7 +968,7 @@ class IdentifiersEdit(QLineEdit): # {{{ tt = self.BASE_TT extra = '' if not isbn: - col = 'rgba(0,255,0,0%)' + col = 'none' elif check_isbn(isbn) is not None: col = 'rgba(0,255,0,20%)' extra = '\n\n'+_('This ISBN number is valid') diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 5f0af1b316..017635c6fb 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -12,7 +12,8 @@ from functools import partial from itertools import izip from PyQt4.Qt import (QIcon, QDialog, QVBoxLayout, QTextBrowser, QSize, - QDialogButtonBox, QApplication, QTimer, QLabel, QProgressBar) + QDialogButtonBox, QApplication, QTimer, QLabel, QProgressBar, + QGridLayout, QPixmap, Qt) from calibre.gui2.dialogs.message_box import MessageBox from calibre.gui2.threaded_jobs import ThreadedJob @@ -25,37 +26,86 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.customize.ui import metadata_plugins from calibre.ptempfile import PersistentTemporaryFile +# Start download {{{ def show_config(gui, parent): from calibre.gui2.preferences import show_config_widget show_config_widget('Sharing', 'Metadata download', parent=parent, gui=gui, never_shutdown=True) -def start_download(gui, ids, callback, identify, covers): - q = MessageBox(MessageBox.QUESTION, _('Schedule download?'), +class ConfirmDialog(QDialog): + + def __init__(self, ids, parent): + QDialog.__init__(self, parent) + self.setWindowTitle(_('Schedule download?')) + self.setWindowIcon(QIcon(I('dialog_question.png'))) + + l = self.l = QGridLayout() + self.setLayout(l) + + i = QLabel(self) + i.setPixmap(QPixmap(I('dialog_question.png'))) + l.addWidget(i, 0, 0) + + t = QLabel( '

'+_('The download of metadata for the %d selected book(s) will' ' run in the background. Proceed?')%len(ids) + '

'+_('You can monitor the progress of the download ' 'by clicking the rotating spinner in the bottom right ' 'corner.') + '

'+_('When the download completes you will be asked for' - ' confirmation before calibre applies the downloaded metadata.'), - show_copy_button=False, parent=gui) - b = q.bb.addButton(_('Configure download'), q.bb.ActionRole) - b.setIcon(QIcon(I('config.png'))) - b.clicked.connect(partial(show_config, gui, q)) - q.det_msg_toggle.setVisible(False) + ' confirmation before calibre applies the downloaded metadata.') + ) + t.setWordWrap(True) + l.addWidget(t, 0, 1) + l.setColumnStretch(0, 1) + l.setColumnStretch(1, 100) - ret = q.exec_() - b.clicked.disconnect() - if ret != q.Accepted: + self.identify = self.covers = True + self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) + self.bb.rejected.connect(self.reject) + b = self.bb.addButton(_('Download only &metadata'), + self.bb.AcceptRole) + b.clicked.connect(self.only_metadata) + b.setIcon(QIcon(I('edit_input.png'))) + b = self.bb.addButton(_('Download only &covers'), + self.bb.AcceptRole) + b.clicked.connect(self.only_covers) + b.setIcon(QIcon(I('default_cover.png'))) + b = self.b = self.bb.addButton(_('&Configure download'), self.bb.ActionRole) + b.setIcon(QIcon(I('config.png'))) + b.clicked.connect(partial(show_config, parent, self)) + l.addWidget(self.bb, 1, 0, 1, 2) + b = self.bb.addButton(_('Download &both'), + self.bb.AcceptRole) + b.clicked.connect(self.accept) + b.setDefault(True) + b.setAutoDefault(True) + b.setIcon(QIcon(I('ok.png'))) + + self.resize(self.sizeHint()) + b.setFocus(Qt.OtherFocusReason) + + def only_metadata(self): + self.covers = False + self.accept() + + def only_covers(self): + self.identify = False + self.accept() + +def start_download(gui, ids, callback): + d = ConfirmDialog(ids, gui) + ret = d.exec_() + d.b.clicked.disconnect() + if ret != d.Accepted: return job = ThreadedJob('metadata bulk download', _('Download metadata for %d books')%len(ids), - download, (ids, gui.current_db, identify, covers), {}, callback) + download, (ids, gui.current_db, d.identify, d.covers), {}, callback) gui.job_manager.run_threaded_job(job) gui.status_bar.show_message(_('Metadata download started'), 3000) - +# }}} class ViewLog(QDialog): # {{{ @@ -93,9 +143,10 @@ def view_log(job, parent): # }}} +# Apply downloaded metadata {{{ class ApplyDialog(QDialog): - def __init__(self, id_map, gui): + def __init__(self, gui): QDialog.__init__(self, gui) self.l = l = QVBoxLayout() @@ -104,27 +155,33 @@ class ApplyDialog(QDialog): self.pb = QProgressBar(self) l.addWidget(self.pb) - self.pb.setMinimum(0) - self.pb.setMaximum(len(id_map)) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) self.bb.rejected.connect(self.reject) - self.bb.accepted.connect(self.accept) l.addWidget(self.bb) self.gui = gui + self.timer = QTimer(self) + self.timer.timeout.connect(self.do_one) + + def start(self, id_map): self.id_map = list(id_map.iteritems()) self.current_idx = 0 - self.failures = [] self.ids = [] self.canceled = False - - QTimer.singleShot(20, self.do_one) + self.pb.setMinimum(0) + self.pb.setMaximum(len(id_map)) + self.timer.start(50) def do_one(self): if self.canceled: return + if self.current_idx >= len(self.id_map): + self.timer.stop() + self.finalize() + return + i, mi = self.id_map[self.current_idx] db = self.gui.current_db try: @@ -144,15 +201,11 @@ class ApplyDialog(QDialog): pass self.pb.setValue(self.pb.value()+1) - - if self.current_idx >= len(self.id_map) - 1: - self.finalize() - else: - self.current_idx += 1 - QTimer.singleShot(20, self.do_one) + self.current_idx += 1 def reject(self): self.canceled = True + self.timer.stop() QDialog.reject(self) def finalize(self): @@ -169,17 +222,18 @@ class ApplyDialog(QDialog): title += ' - ' + authors_to_string(authors) msg.append(title+'\n\n'+tb+'\n'+('*'*80)) - error_dialog(self, _('Some failures'), + parent = self if self.isVisible() else self.parent() + error_dialog(parent, _('Some failures'), _('Failed to apply updated metadata for some books' ' in your library. Click "Show Details" to see ' 'details.'), det_msg='\n\n'.join(msg), show=True) - self.accept() if self.ids: cr = self.gui.library_view.currentIndex().row() self.gui.library_view.model().refresh_ids( self.ids, cr) if self.gui.cover_flow: self.gui.cover_flow.dataChanged() + self.accept() _amd = None def apply_metadata(job, gui, q, result): @@ -188,7 +242,7 @@ def apply_metadata(job, gui, q, result): q.finished.disconnect() if result != q.Accepted: return - id_map, failed_ids, failed_covers, title_map = job.result + id_map, failed_ids, failed_covers, title_map, all_failed = job.result id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in failed_ids]) if not id_map: @@ -217,41 +271,55 @@ def apply_metadata(job, gui, q, result): 'Do you want to proceed?'), det_msg='\n'.join(modified)): return - _amd = ApplyDialog(id_map, gui) - _amd.exec_() + if _amd is None: + _amd = ApplyDialog(gui) + _amd.start(id_map) + if len(id_map) > 3: + _amd.exec_() def proceed(gui, job): gui.status_bar.show_message(_('Metadata download completed'), 3000) - id_map, failed_ids, failed_covers, title_map = job.result - fmsg = det_msg = '' - if failed_ids or failed_covers: - fmsg = '

'+_('Could not download metadata and/or covers for %d of the books. Click' - ' "Show details" to see which books.')%len(failed_ids) - det_msg = [] - for i in failed_ids | failed_covers: - title = title_map[i] - if i in failed_ids: - title += (' ' + _('(Failed metadata)')) - if i in failed_covers: - title += (' ' + _('(Failed cover)')) - det_msg.append(title) - msg = '

' + _('Finished downloading metadata for %d book(s). ' - 'Proceed with updating the metadata in your library?')%len(id_map) - q = MessageBox(MessageBox.QUESTION, _('Download complete'), - msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), - parent=gui) + id_map, failed_ids, failed_covers, title_map, all_failed = job.result + det_msg = [] + for i in failed_ids | failed_covers: + title = title_map[i] + if i in failed_ids: + title += (' ' + _('(Failed metadata)')) + if i in failed_covers: + title += (' ' + _('(Failed cover)')) + det_msg.append(title) + det_msg = '\n'.join(det_msg) + + if all_failed: + q = error_dialog(gui, _('Download failed'), + _('Failed to download metadata or covers for any of the %d' + ' book(s).') % len(id_map), det_msg=det_msg) + else: + fmsg = '' + if failed_ids or failed_covers: + fmsg = '

'+_('Could not download metadata and/or covers for %d of the books. Click' + ' "Show details" to see which books.')%len(failed_ids) + msg = '

' + _('Finished downloading metadata for %d book(s). ' + 'Proceed with updating the metadata in your library?')%len(id_map) + q = MessageBox(MessageBox.QUESTION, _('Download complete'), + msg + fmsg, det_msg=det_msg, show_copy_button=bool(failed_ids), + parent=gui) + q.finished.connect(partial(apply_metadata, job, gui, q)) + q.vlb = q.bb.addButton(_('View log'), q.bb.ActionRole) q.vlb.setIcon(QIcon(I('debug.png'))) q.vlb.clicked.connect(partial(view_log, job, q)) q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers)) q.setModal(False) q.show() - q.finished.connect(partial(apply_metadata, job, gui, q)) + +# }}} def merge_result(oldmi, newmi): dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: - setattr(newmi, f, getattr(dummy, f)) + if ':' not in f: + setattr(newmi, f, getattr(dummy, f)) fields = set() for plugin in metadata_plugins(['identify']): fields |= plugin.touched_fields @@ -276,6 +344,7 @@ def download(ids, db, do_identify, covers, title_map = {} ans = {} count = 0 + all_failed = True for i, mi in izip(ids, metadata): if abort.is_set(): log.error('Aborting...') @@ -290,6 +359,7 @@ def download(ids, db, do_identify, covers, except: pass if results: + all_failed = False mi = merge_result(mi, results[0]) identifiers = mi.identifiers if not mi.is_null('rating'): @@ -307,6 +377,7 @@ def download(ids, db, do_identify, covers, with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f: f.write(cdata[-1]) mi.cover = f.name + all_failed = False else: failed_covers.add(i) ans[i] = mi @@ -314,7 +385,7 @@ def download(ids, db, do_identify, covers, notifications.put((count/len(ids), _('Downloaded %d of %d')%(count, len(ids)))) log('Download complete, with %d failures'%len(failed_ids)) - return (ans, failed_ids, failed_covers, title_map) + return (ans, failed_ids, failed_covers, title_map, all_failed) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 52b9e99872..63d4499966 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -156,6 +156,9 @@ class MetadataSingleDialogBase(ResizableDialog): self.identifiers = IdentifiersEdit(self) self.basic_metadata_widgets.append(self.identifiers) + self.clear_identifiers_button = QToolButton(self) + self.clear_identifiers_button.setIcon(QIcon(I('trash.png'))) + self.clear_identifiers_button.clicked.connect(self.identifiers.clear) self.publisher = PublisherEdit(self) self.basic_metadata_widgets.append(self.publisher) @@ -323,7 +326,8 @@ class MetadataSingleDialogBase(ResizableDialog): mi = d.book dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: - setattr(mi, f, getattr(dummy, f)) + if ':' not in f: + setattr(mi, f, getattr(dummy, f)) if mi is not None: self.update_from_mi(mi) if d.cover_pixmap is not None: @@ -541,8 +545,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ sto(self.rating, self.tags) create_row2(2, self.tags, self.tags_editor_button) sto(self.tags_editor_button, self.identifiers) - create_row2(3, self.identifiers) - sto(self.identifiers, self.timestamp) + create_row2(3, self.identifiers, self.clear_identifiers_button) + sto(self.clear_identifiers_button, self.timestamp) create_row2(4, self.timestamp, self.timestamp.clear_button) sto(self.timestamp.clear_button, self.pubdate) create_row2(5, self.pubdate, self.pubdate.clear_button) @@ -657,7 +661,8 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ create_row(9, self.publisher, self.timestamp) create_row(10, self.timestamp, self.identifiers, button=self.timestamp.clear_button, icon='trash.png') - create_row(11, self.identifiers, self.comments) + create_row(11, self.identifiers, self.comments, + button=self.clear_identifiers_button, icon='trash.png') tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), 12, 1, 1 ,1) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 8f01c6df1e..a3ac777115 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -116,6 +116,10 @@ class CoverDelegate(QStyledItemDelegate): # {{{ def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) + # Ensure the cover is rendered over any selection rect + style = QApplication.style() + style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter, + QPixmap(index.data(Qt.DecorationRole))) if self.timer.isActive() and index.data(Qt.UserRole).toBool(): rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index 649a58448d..5b0a05ba40 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -337,7 +337,13 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, bb.button(bb.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults) bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).clicked.connect(d.accept) - w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True)) + def onchange(): + b = bb.button(bb.Apply) + b.setEnabled(True) + b.setDefault(True) + b.setAutoDefault(True) + w.changed_signal.connect(onchange) + bb.button(bb.Cancel).setFocus(True) l = QVBoxLayout() d.setLayout(l) l.addWidget(w) diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py index 330332a716..ead5da4ce4 100644 --- a/src/calibre/gui2/preferences/misc.py +++ b/src/calibre/gui2/preferences/misc.py @@ -6,19 +6,27 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.constants import isosx -# Check Integrity {{{ +class WorkersSetting(Setting): + + def set_gui_val(self, val): + val = val//2 + Setting.set_gui_val(self, val) + + def get_gui_val(self): + val = Setting.get_gui_val(self) + return val * 2 class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): self.gui = gui r = self.register - r('worker_limit', config, restart_required=True) + r('worker_limit', config, restart_required=True, setting=WorkersSetting) r('enforce_cpu_limit', config, restart_required=True) self.device_detection_button.clicked.connect(self.debug_device_detection) self.button_open_config_dir.clicked.connect(self.open_config_dir) diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui index c036cb971b..8b0189b0a1 100644 --- a/src/calibre/gui2/preferences/misc.ui +++ b/src/calibre/gui2/preferences/misc.ui @@ -17,7 +17,7 @@ - &Maximum number of waiting worker processes (needs restart): + Max. simultaneous conversion/news download jobs: opt_worker_limit @@ -27,13 +27,7 @@ - 2 - - - 10000 - - - 2 + 1 diff --git a/src/calibre/gui2/preferences/plugboard.py b/src/calibre/gui2/preferences/plugboard.py index 8f2b084d76..1ce4f89dfd 100644 --- a/src/calibre/gui2/preferences/plugboard.py +++ b/src/calibre/gui2/preferences/plugboard.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import Qt, QLineEdit, QComboBox, SIGNAL, QListWidgetItem +from calibre.customize.ui import is_disabled from calibre.gui2 import error_dialog from calibre.gui2.device import device_name_for_plugboards from calibre.gui2.dialogs.template_dialog import TemplateDialog @@ -15,6 +16,8 @@ from calibre.gui2.preferences.plugboard_ui import Ui_Form from calibre.customize.ui import metadata_writers, device_plugins from calibre.library.save_to_disk import plugboard_any_format_value, \ plugboard_any_device_value, plugboard_save_to_disk_value +from calibre.library.server.content import plugboard_content_server_value, \ + plugboard_content_server_formats from calibre.utils.formatter import validation_formatter @@ -68,19 +71,26 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.device_label.setText(_('Device currently connected: None')) self.devices = ['', 'APPLE', 'FOLDER_DEVICE'] + self.device_to_formats_map = {} for device in device_plugins(): n = device_name_for_plugboards(device) + self.device_to_formats_map[n] = device.FORMATS if n not in self.devices: self.devices.append(n) self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) self.devices.insert(1, plugboard_save_to_disk_value) - self.devices.insert(2, plugboard_any_device_value) + self.devices.insert(1, plugboard_content_server_value) + self.device_to_formats_map[plugboard_content_server_value] = \ + plugboard_content_server_formats + self.devices.insert(1, plugboard_any_device_value) self.new_device.addItems(self.devices) self.formats = [''] for w in metadata_writers(): - for f in w.file_types: - self.formats.append(f) + if not is_disabled(w): + for f in w.file_types: + if not f in self.formats: + self.formats.append(f) self.formats.append('device_db') self.formats.sort() self.formats.insert(1, plugboard_any_format_value) @@ -230,6 +240,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): show=True) self.new_device.setCurrentIndex(0) return + if self.current_device in self.device_to_formats_map: + allowable_formats = self.device_to_formats_map[self.current_device] + if self.current_format not in allowable_formats: + error_dialog(self, '', + _('The {0} device does not support the {1} format.'). + format(self.current_device, self.current_format), + show=True) + self.new_device.setCurrentIndex(0) + return self.set_fields() def new_format_changed(self, txt): diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 79cd2b1ce4..8888a64e84 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -13,9 +13,9 @@ from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \ from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.plugins_ui import Ui_Form -from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ - disable_plugin, plugin_customization, add_plugin, \ - remove_plugin +from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin, + disable_plugin, plugin_customization, add_plugin, + remove_plugin, NameConflict) from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \ question_dialog, gprefs from calibre.utils.search_query_parser import SearchQueryParser @@ -279,7 +279,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): ' Are you sure you want to proceed?'), show_copy_button=False): return - plugin = add_plugin(path) + try: + plugin = add_plugin(path) + except NameConflict as e: + return error_dialog(self, _('Already exists'), + unicode(e), show=True) self._plugin_model.populate() self._plugin_model.reset() self.changed_signal.emit() diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 8ef02b34b0..ffebc9e131 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -17,6 +17,10 @@ class SearchRestrictionMixin(object): self.search_restriction.setMinimumContentsLength(10) self.search_restriction.setStatusTip(self.search_restriction.toolTip()) self.search_count.setText(_("(all books)")) + self.search_restriction_tooltip = \ + _('Books display will be restricted to those matching a ' + 'selected saved search') + self.search_restriction.setToolTip(self.search_restriction_tooltip) def apply_named_search_restriction(self, name): if not name: @@ -30,29 +34,38 @@ class SearchRestrictionMixin(object): self.apply_search_restriction(r) def apply_text_search_restriction(self, search): + search = unicode(search) if not search: - self.search_restriction.setItemText(1, _('*Current search')) self.search_restriction.setCurrentIndex(0) else: - self.search_restriction.setCurrentIndex(1) - self.search_restriction.setItemText(1, search) + s = '*' + search + if self.search_restriction.count() > 1: + txt = unicode(self.search_restriction.itemText(2)) + if txt.startswith('*'): + self.search_restriction.setItemText(2, s) + else: + self.search_restriction.insertItem(2, s) + else: + self.search_restriction.insertItem(2, s) + self.search_restriction.setCurrentIndex(2) + self.search_restriction.setToolTip('

' + + self.search_restriction_tooltip + + _(' or the search ') + "'" + search + "'

") self._apply_search_restriction(search) def apply_search_restriction(self, i): - self.search_restriction.setItemText(1, _('*Current search')) if i == 1: - restriction = unicode(self.search.currentText()) - if not restriction: - self.search_restriction.setCurrentIndex(0) - else: - self.search_restriction.setItemText(1, restriction) + self.apply_text_search_restriction(unicode(self.search.currentText())) + elif i == 2 and unicode(self.search_restriction.currentText()).startswith('*'): + self.apply_text_search_restriction( + unicode(self.search_restriction.currentText())[1:]) else: r = unicode(self.search_restriction.currentText()) if r is not None and r != '': restriction = 'search:"%s"'%(r) else: restriction = '' - self._apply_search_restriction(restriction) + self._apply_search_restriction(restriction) def _apply_search_restriction(self, restriction): self.saved_search.clear() diff --git a/src/calibre/gui2/store/amazon_plugin.py b/src/calibre/gui2/store/amazon_plugin.py index d5d8b54600..a68e4611f0 100644 --- a/src/calibre/gui2/store/amazon_plugin.py +++ b/src/calibre/gui2/store/amazon_plugin.py @@ -154,6 +154,13 @@ class AmazonKindleStore(StorePlugin): cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href) if cover_img: cover_url = cover_img[0] + parts = cover_url.split('/') + bn = parts[-1] + f, _, ext = bn.rpartition('.') + if '_' in f: + bn = f.partition('_')[0]+'_SL160_.'+ext + parts[-1] = bn + cover_url = '/'.join(parts) title = ''.join(data.xpath('div[@class="productTitle"]/a/text()')) author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) @@ -174,7 +181,7 @@ class AmazonKindleStore(StorePlugin): def get_details(self, search_result, timeout): url = 'http://amazon.com/dp/' - + br = browser() with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: idata = html.fromstring(nf.read()) @@ -187,4 +194,4 @@ class AmazonKindleStore(StorePlugin): search_result.drm = SearchResult.DRM_LOCKED return True - \ No newline at end of file + diff --git a/src/calibre/gui2/store/search.ui b/src/calibre/gui2/store/search.ui index 16fc0c4deb..dd7db939a4 100644 --- a/src/calibre/gui2/store/search.ui +++ b/src/calibre/gui2/store/search.ui @@ -11,7 +11,11 @@ - calibre Store Search + Get Books + + + + :/images/store.png:/images/store.png true @@ -58,8 +62,8 @@ 0 0 - 215 - 116 + 170 + 138 @@ -174,7 +178,9 @@ - + + + close diff --git a/src/calibre/gui2/store/web_control.py b/src/calibre/gui2/store/web_control.py index 58711d786f..17b42c5643 100644 --- a/src/calibre/gui2/store/web_control.py +++ b/src/calibre/gui2/store/web_control.py @@ -70,16 +70,17 @@ class NPWebView(QWebView): if ext not in BOOK_EXTENSIONS: if ext == 'acsm': from calibre.gui2.dialogs.confirm_delete import confirm - confirm(_('

You have selected to download an ebook that uses ' - 'the ACSM format. ACSM files are not ebook files but ' - 'pointers ' + if not confirm('

' + _('This ebook is a DRMed EPUB file. ' + 'You will be prompted to save this file to your ' + 'computer. Once it is saved, open it with ' '' - 'Adobe Digital Editions (ADE) uses so it can download ' - 'the ebook and apply DRM to it. You will be prompted to save this ' - 'file to your computer. Then open it using ADE. Once ADE has ' - 'downloaded the actual ebook you can add the book to calibre ' - 'using Add and selecting the file from the ADE library folder.'), - 'acsm_download', self) + 'Adobe Digital Editions (ADE).

ADE, in turn ' + 'will download the actual ebook, which will be a ' + '.epub file. You can add this book to calibre ' + 'using "Add Books" and selecting the file from ' + 'the ADE library folder.'), + 'acsm_download', self): + return home = os.path.expanduser('~') name = QFileDialog.getSaveFileName(self, _('File is not a supported ebook type. Save to disk?'), diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index ea0d2570e5..a7ecdf7b88 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -19,7 +19,6 @@ from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.gui2.filename_pattern_ui import Ui_Form from calibre import fit_image from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.utils.config import prefs, XMLConfig, tweaks from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ @@ -95,6 +94,7 @@ class FilenamePattern(QWidget, Ui_Form): self.re.setCurrentIndex(0) def do_test(self): + from calibre.ebooks.metadata.meta import metadata_from_filename try: pat = self.pattern() except Exception as err: diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 9523795f28..92c5ca9b3c 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -707,7 +707,10 @@ class ResultCache(SearchQueryParser): # {{{ for loc in location: # location is now an array of field indices if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query - q = query.replace(',', '|'); + if matchkind == REGEXP_MATCH: + q = query.replace(',', r'\|'); + else: + q = query.replace(',', '|'); else: q = query diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index ffa08eaed2..717e8e2c6b 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -15,7 +15,6 @@ from calibre.customize import CatalogPlugin from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.ebooks.chardet import substitute_entites -from calibre.ebooks.oeb.base import XHTML_NS from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.config import config_dir from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf @@ -4322,6 +4321,8 @@ Author '{0}': ''' Generate description header from template ''' + from calibre.ebooks.oeb.base import XHTML_NS + def generate_html(): args = dict( author=author, diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index b1a8236151..61e7ec334d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -10,8 +10,7 @@ Command line interface to the calibre database. import sys, os, cStringIO, re from textwrap import TextWrapper -from calibre import terminal_controller, preferred_encoding, prints, \ - isbytestring +from calibre import preferred_encoding, prints, isbytestring from calibre.utils.config import OptionParser, prefs, tweaks from calibre.ebooks.metadata.meta import get_metadata from calibre.library.database2 import LibraryDatabase2 @@ -53,6 +52,8 @@ def get_db(dbpath, options): def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator, prefix, subtitle='Books in the calibre database'): + from calibre.constants import terminal_controller as tc + terminal_controller = tc() if sort_by: db.sort(sort_by, ascending) if search_text: @@ -1087,6 +1088,9 @@ def command_list_categories(args, dbpath): fields = ['category', 'tag_name', 'count', 'rating'] def do_list(): + from calibre.constants import terminal_controller as tc + terminal_controller = tc() + separator = ' ' widths = list(map(lambda x : 0, fields)) for i in data: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bdcefd13a2..d7f6c22925 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -15,7 +15,8 @@ from math import ceil from PyQt4.QtGui import QImage from calibre import prints -from calibre.ebooks.metadata import title_sort, author_to_author_sort +from calibre.ebooks.metadata import (title_sort, author_to_author_sort, + string_to_authors, authors_to_string) from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.library.database import LibraryDatabase from calibre.library.field_metadata import FieldMetadata, TagsIcons @@ -24,9 +25,7 @@ from calibre.library.caches import ResultCache from calibre.library.custom_columns import CustomColumns from calibre.library.sqlite import connect, IntegrityError from calibre.library.prefs import DBPrefs -from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata.book.base import Metadata -from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.ptempfile import PersistentTemporaryFile from calibre.customize.ui import run_plugins_on_import @@ -853,6 +852,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.pubdate = row[fm['pubdate']] mi.uuid = row[fm['uuid']] mi.title_sort = row[fm['sort']] + mi.book_size = row[fm['size']] mi.last_modified = row[fm['last_modified']] formats = row[fm['formats']] if not formats: @@ -1378,13 +1378,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for (cat, dex, mult, is_comp) in md: if not book[dex]: continue + tid_cat = tids[cat] + tcats_cat = tcategories[cat] if not mult: val = book[dex] if is_comp: - item = tcategories[cat].get(val, None) + item = tcats_cat.get(val, None) if not item: item = tag_class(val, val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id = val if rating > 0: @@ -1392,11 +1394,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): item.rc += 1 continue try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id @@ -1410,21 +1412,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if is_comp: vals = [v.strip() for v in vals if v.strip()] for val in vals: - if val not in tids: - tids[cat][val] = (val, val) - item = tcategories[cat].get(val, None) - if not item: - item = tag_class(val, val) - tcategories[cat][val] = item - item.c += 1 - item.id = val + if val not in tid_cat: + tid_cat[val] = (val, val) for val in vals: try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id @@ -2732,6 +2728,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.set_identifier(id_, 'isbn', isbn, notify=notify, commit=commit) def add_catalog(self, path, title): + from calibre.ebooks.metadata.meta import get_metadata + format = os.path.splitext(path)[1][1:].lower() with lopen(path, 'rb') as stream: matches = self.data.get_matches('title', '='+title) @@ -2767,6 +2765,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def add_news(self, path, arg): + from calibre.ebooks.metadata.meta import get_metadata + format = os.path.splitext(path)[1][1:].lower() stream = path if hasattr(path, 'read') else lopen(path, 'rb') stream.seek(0) @@ -3160,6 +3160,8 @@ books_series_link feeds yield formats def import_book_directory_multiple(self, dirpath, callback=None): + from calibre.ebooks.metadata.meta import metadata_from_formats + duplicates = [] for formats in self.find_books_in_directory(dirpath, False): mi = metadata_from_formats(formats) @@ -3175,6 +3177,7 @@ books_series_link feeds return duplicates def import_book_directory(self, dirpath, callback=None): + from calibre.ebooks.metadata.meta import metadata_from_formats dirpath = os.path.abspath(dirpath) formats = self.find_books_in_directory(dirpath, True) formats = list(formats)[0] diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 33929ac2e4..374505c467 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -35,7 +35,7 @@ category_icon_map = { 'custom:' : 'column.png', 'user:' : 'tb_folder.png', 'search' : 'search.png', - 'identifiers': 'id_card.png' + 'identifiers': 'identifiers.png' } diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 96c42e6e0e..f7f5559412 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -14,7 +14,6 @@ from calibre.utils.formatter import TemplateFormatter from calibre.utils.filenames import shorten_components_to, supports_long_names, \ ascii_filename from calibre.ebooks.metadata.opf2 import metadata_to_opf -from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import preferred_encoding from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import title_sort @@ -51,6 +50,23 @@ for x in FORMAT_ARG_DESCS: FORMAT_ARGS[x] = '' +def find_plugboard(device_name, format, plugboards): + cpb = None + if format in plugboards: + cpb = plugboards[format] + elif plugboard_any_format_value in plugboards: + cpb = plugboards[plugboard_any_format_value] + if cpb is not None: + if device_name in cpb: + cpb = cpb[device_name] + elif plugboard_any_device_value in cpb: + cpb = cpb[plugboard_any_device_value] + else: + cpb = None + if DEBUG: + prints('Device using plugboard', format, device_name, cpb) + return cpb + def config(defaults=None): if defaults is None: c = Config('save_to_disk', _('Options to control saving to disk')) @@ -181,7 +197,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250, for key in custom_metadata: if key in format_args: cm = custom_metadata[key] - ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't... if cm['datatype'] == 'series': format_args[key] = title_sort(format_args[key], order=tsorder) if key+'_index' in format_args: @@ -235,6 +250,7 @@ def save_book_to_disk(id_, db, root, opts, length): def do_save_book_to_disk(id_, mi, cover, plugboards, format_map, root, opts, length): + from calibre.ebooks.metadata.meta import set_metadata available_formats = [x.lower().strip() for x in format_map.keys()] if opts.formats == 'all': asked_formats = available_formats @@ -279,20 +295,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards, written = False for fmt in formats: global plugboard_save_to_disk_value, plugboard_any_format_value - dev_name = plugboard_save_to_disk_value - cpb = None - if fmt in plugboards: - cpb = plugboards[fmt] - if dev_name in cpb: - cpb = cpb[dev_name] - else: - cpb = None - if cpb is None and plugboard_any_format_value in plugboards: - cpb = plugboards[plugboard_any_format_value] - if dev_name in cpb: - cpb = cpb[dev_name] - else: - cpb = None + cpb = find_plugboard(plugboard_save_to_disk_value, fmt, plugboards) # Leave this here for a while, in case problems arise. if cpb is not None: prints('Save-to-disk using plugboard:', fmt, cpb) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 0c3edd1627..08de4faecd 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -12,9 +12,14 @@ import cherrypy from calibre import fit_image, guess_type from calibre.utils.date import fromtimestamp from calibre.library.caches import SortKeyGenerator +from calibre.library.save_to_disk import find_plugboard + from calibre.utils.magick.draw import save_cover_data_to, Image, \ thumbnail as generate_thumbnail +plugboard_content_server_value = 'content_server' +plugboard_content_server_formats = ['epub'] + class CSSortKeyGenerator(SortKeyGenerator): def __init__(self, fields, fm, db_prefs): @@ -183,16 +188,30 @@ class ContentServer(object): if fmt is None: raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format)) if format == 'EPUB': + # Get the original metadata + mi = self.db.get_metadata(id, index_is_id=True) + + # Get any EPUB plugboards for the content server + plugboards = self.db.prefs.get('plugboards', {}) + cpb = find_plugboard(plugboard_content_server_value, + 'epub', plugboards) + if cpb: + # Transform the metadata via the plugboard + newmi = mi.deepcopy_metadata() + newmi.template_to_attribute(mi, cpb) + else: + newmi = mi + + # Write the updated file from tempfile import TemporaryFile from calibre.ebooks.metadata.meta import set_metadata raw = fmt.read() fmt = TemporaryFile() fmt.write(raw) fmt.seek(0) - set_metadata(fmt, self.db.get_metadata(id, index_is_id=True, - get_cover=True), - 'epub') + set_metadata(fmt, newmi, 'epub') fmt.seek(0) + mt = guess_type('dummy.'+format.lower())[0] if mt is None: mt = 'application/octet-stream' diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index ef4da23826..3dce13f144 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -549,17 +549,6 @@ How do I run calibre from my USB stick? A portable version of calibre is available at: `portableapps.com `_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions `. -Why are there so many calibre-parallel processes on my system? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -|app| maintains two separate worker process pools. One is used for adding books/saving to disk and the other for conversions. You can control the number of worker processes via :guilabel:`Preferences->Advanced->Miscellaneous`. So if you set it to 6 that means a maximum of 3 conversions will run simultaneously. And that is why you will see the number of worker processes changes by two when you use the up and down arrows. On windows, you can set the priority that these processes run with. This can be useful on older, single CPU machines, if you find them slowing down to a crawl when conversions are running. - -In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously. - -And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run in the GUI thread of the main process or in a separate process. - -Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes. - How do I run parts of |app| like news download and the content server on my own linux server? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index cdb8df2e2b..a77f0d1697 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -230,6 +230,7 @@ The following functions are available in addition to those described in single-f * ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression + * ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. diff --git a/src/calibre/startup.py b/src/calibre/startup.py index c883c43e8a..78f8aff7e3 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -61,6 +61,12 @@ if not _run_once: ################################################################################ # Initialize locale + # Import string as we do not want locale specific + # string.whitespace/printable, on windows especially, this causes problems. + # Before the delay load optimizations, string was loaded before this point + # anyway, so we preserve the old behavior explicitly. + import string + string try: locale.setlocale(locale.LC_ALL, '') except: @@ -163,10 +169,6 @@ if not _run_once: __builtin__.__dict__['icu_upper'] = icu_upper __builtin__.__dict__['icu_title'] = title_case - import mimetypes - mimetypes.init([P('mime.types')]) - guess_type = mimetypes.guess_type - def test_lopen(): from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 66316d051b..8b23cf3071 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -6,22 +6,25 @@ __docformat__ = 'restructuredtext en' ''' Manage application-wide preferences. ''' -import os, re, cPickle, textwrap, traceback, plistlib, json, base64, datetime +import os, re, cPickle, base64, datetime, json, plistlib from copy import deepcopy -from functools import partial from optparse import OptionParser as _OptionParser from optparse import IndentedHelpFormatter -from collections import defaultdict -from calibre.constants import terminal_controller, config_dir, CONFIG_DIR_MODE, \ - __appname__, __version__, __author__ -from calibre.utils.lock import LockError, ExclusiveFile +from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__, + __version__, __author__, terminal_controller) +from calibre.utils.lock import ExclusiveFile +from calibre.utils.config_base import (make_config_dir, Option, OptionValues, + OptionSet, ConfigInterface, Config, prefs, StringConfig, ConfigProxy, + read_raw_tweaks, read_tweaks, write_tweaks, tweaks, plugin_dir) -plugin_dir = os.path.join(config_dir, 'plugins') +if False: + # Make pyflakes happy + Config, ConfigProxy, Option, OptionValues, StringConfig + OptionSet, ConfigInterface, read_tweaks, write_tweaks + read_raw_tweaks, tweaks, plugin_dir -def make_config_dir(): - if not os.path.exists(plugin_dir): - os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) +test_eight_code = tweaks.get('test_eight_code', False) def check_config_write_access(): return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK) @@ -29,23 +32,28 @@ def check_config_write_access(): class CustomHelpFormatter(IndentedHelpFormatter): def format_usage(self, usage): - return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) + tc = terminal_controller() + return _("%sUsage%s: %s\n") % (tc.BLUE, tc.NORMAL, usage) def format_heading(self, heading): - return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, - "", heading, terminal_controller.NORMAL) + tc = terminal_controller() + return "%*s%s%s%s:\n" % (self.current_indent, tc.BLUE, + "", heading, tc.NORMAL) def format_option(self, option): + import textwrap + tc = terminal_controller() + result = [] opts = self.option_strings[option] opt_width = self.help_position - self.current_indent - 2 if len(opts) > opt_width: opts = "%*s%s\n" % (self.current_indent, "", - terminal_controller.GREEN+opts+terminal_controller.NORMAL) + tc.GREEN+opts+tc.NORMAL) indent_first = self.help_position else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), - terminal_controller.GREEN + opts + terminal_controller.NORMAL) + opts = "%*s%-*s " % (self.current_indent, "", opt_width + + len(tc.GREEN + tc.NORMAL), tc.GREEN + opts + tc.NORMAL) indent_first = 0 result.append(opts) if option.help: @@ -71,9 +79,12 @@ class OptionParser(_OptionParser): gui_mode=False, conflict_handler='resolve', **kwds): + import textwrap + tc = terminal_controller() + usage = textwrap.dedent(usage) if epilog is None: - epilog = _('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL + epilog = _('Created by ')+tc.RED+__author__+tc.NORMAL usage += '\n\n'+_('''Whenever you pass arguments to %prog that have spaces in them, ''' '''enclose the arguments in quotation marks.''') _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, @@ -146,353 +157,6 @@ class OptionParser(_OptionParser): upper.__dict__[dest] = lower.__dict__[dest] - -class Option(object): - - def __init__(self, name, switches=[], help='', type=None, choices=None, - check=None, group=None, default=None, action=None, metavar=None): - if choices: - type = 'choice' - - self.name = name - self.switches = switches - self.help = help.replace('%default', repr(default)) if help else None - self.type = type - if self.type is None and action is None and choices is None: - if isinstance(default, float): - self.type = 'float' - elif isinstance(default, int) and not isinstance(default, bool): - self.type = 'int' - - self.choices = choices - self.check = check - self.group = group - self.default = default - self.action = action - self.metavar = metavar - - def __eq__(self, other): - return self.name == getattr(other, 'name', other) - - def __repr__(self): - return 'Option: '+self.name - - def __str__(self): - return repr(self) - -class OptionValues(object): - - def copy(self): - return deepcopy(self) - -class OptionSet(object): - - OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', - re.DOTALL|re.IGNORECASE) - - def __init__(self, description=''): - self.description = description - self.defaults = {} - self.preferences = [] - self.group_list = [] - self.groups = {} - self.set_buffer = {} - - def has_option(self, name_or_option_object): - if name_or_option_object in self.preferences: - return True - for p in self.preferences: - if p.name == name_or_option_object: - return True - return False - - def get_option(self, name_or_option_object): - idx = self.preferences.index(name_or_option_object) - if idx > -1: - return self.preferences[idx] - for p in self.preferences: - if p.name == name_or_option_object: - return p - - def add_group(self, name, description=''): - if name in self.group_list: - raise ValueError('A group by the name %s already exists in this set'%name) - self.groups[name] = description - self.group_list.append(name) - return partial(self.add_opt, group=name) - - def update(self, other): - for name in other.groups.keys(): - self.groups[name] = other.groups[name] - if name not in self.group_list: - self.group_list.append(name) - for pref in other.preferences: - if pref in self.preferences: - self.preferences.remove(pref) - self.preferences.append(pref) - - def smart_update(self, opts1, opts2): - ''' - Updates the preference values in opts1 using only the non-default preference values in opts2. - ''' - for pref in self.preferences: - new = getattr(opts2, pref.name, pref.default) - if new != pref.default: - setattr(opts1, pref.name, new) - - def remove_opt(self, name): - if name in self.preferences: - self.preferences.remove(name) - - - def add_opt(self, name, switches=[], help=None, type=None, choices=None, - group=None, default=None, action=None, metavar=None): - ''' - Add an option to this section. - - :param name: The name of this option. Must be a valid Python identifier. - Must also be unique in this OptionSet and all its subsets. - :param switches: List of command line switches for this option - (as supplied to :module:`optparse`). If empty, this - option will not be added to the command line parser. - :param help: Help text. - :param type: Type checking of option values. Supported types are: - `None, 'choice', 'complex', 'float', 'int', 'string'`. - :param choices: List of strings or `None`. - :param group: Group this option belongs to. You must previously - have created this group with a call to :method:`add_group`. - :param default: The default value for this option. - :param action: The action to pass to optparse. Supported values are: - `None, 'count'`. For choices and boolean options, - action is automatically set correctly. - ''' - pref = Option(name, switches=switches, help=help, type=type, choices=choices, - group=group, default=default, action=action, metavar=None) - if group is not None and group not in self.groups.keys(): - raise ValueError('Group %s has not been added to this section'%group) - if pref in self.preferences: - raise ValueError('An option with the name %s already exists in this set.'%name) - self.preferences.append(pref) - self.defaults[name] = default - - def option_parser(self, user_defaults=None, usage='', gui_mode=False): - parser = OptionParser(usage, gui_mode=gui_mode) - groups = defaultdict(lambda : parser) - for group, desc in self.groups.items(): - groups[group] = parser.add_option_group(group.upper(), desc) - - for pref in self.preferences: - if not pref.switches: - continue - g = groups[pref.group] - action = pref.action - if action is None: - action = 'store' - if pref.default is True or pref.default is False: - action = 'store_' + ('false' if pref.default else 'true') - args = dict( - dest=pref.name, - help=pref.help, - metavar=pref.metavar, - type=pref.type, - choices=pref.choices, - default=getattr(user_defaults, pref.name, pref.default), - action=action, - ) - g.add_option(*pref.switches, **args) - - - return parser - - def get_override_section(self, src): - match = self.OVERRIDE_PAT.search(src) - if match: - return match.group() - return '' - - def parse_string(self, src): - options = {'cPickle':cPickle} - if src is not None: - try: - if not isinstance(src, unicode): - src = src.decode('utf-8') - exec src in options - except: - print 'Failed to parse options string:' - print repr(src) - traceback.print_exc() - opts = OptionValues() - for pref in self.preferences: - val = options.get(pref.name, pref.default) - formatter = __builtins__.get(pref.type, None) - if callable(formatter): - val = formatter(val) - setattr(opts, pref.name, val) - - return opts - - def render_group(self, name, desc, opts): - prefs = [pref for pref in self.preferences if pref.group == name] - lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] - if desc: - lines += map(lambda x: '# '+x, desc.split('\n')) - lines.append(' ') - for pref in prefs: - lines.append('# '+pref.name.replace('_', ' ')) - if pref.help: - lines += map(lambda x: '# ' + x, pref.help.split('\n')) - lines.append('%s = %s'%(pref.name, - self.serialize_opt(getattr(opts, pref.name, pref.default)))) - lines.append(' ') - return '\n'.join(lines) - - def serialize_opt(self, val): - if val is val is True or val is False or val is None or \ - isinstance(val, (int, float, long, basestring)): - return repr(val) - from PyQt4.QtCore import QString - if isinstance(val, QString): - return repr(unicode(val)) - pickle = cPickle.dumps(val, -1) - return 'cPickle.loads(%s)'%repr(pickle) - - def serialize(self, opts): - src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) - groups = [self.render_group(name, self.groups.get(name, ''), opts) \ - for name in [None] + self.group_list] - return src + '\n\n'.join(groups) - -class ConfigInterface(object): - - def __init__(self, description): - self.option_set = OptionSet(description=description) - self.add_opt = self.option_set.add_opt - self.add_group = self.option_set.add_group - self.remove_opt = self.remove = self.option_set.remove_opt - self.parse_string = self.option_set.parse_string - self.get_option = self.option_set.get_option - self.preferences = self.option_set.preferences - - def update(self, other): - self.option_set.update(other.option_set) - - def option_parser(self, usage='', gui_mode=False): - return self.option_set.option_parser(user_defaults=self.parse(), - usage=usage, gui_mode=gui_mode) - - def smart_update(self, opts1, opts2): - self.option_set.smart_update(opts1, opts2) - - -class Config(ConfigInterface): - ''' - A file based configuration. - ''' - - def __init__(self, basename, description=''): - ConfigInterface.__init__(self, description) - self.config_file_path = os.path.join(config_dir, basename+'.py') - - - def parse(self): - src = '' - if os.path.exists(self.config_file_path): - try: - with ExclusiveFile(self.config_file_path) as f: - try: - src = f.read().decode('utf-8') - except ValueError: - print "Failed to parse", self.config_file_path - traceback.print_exc() - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - return self.option_set.parse_string(src) - - def as_string(self): - if not os.path.exists(self.config_file_path): - return '' - try: - with ExclusiveFile(self.config_file_path) as f: - return f.read().decode('utf-8') - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - - def set(self, name, val): - if not self.option_set.has_option(name): - raise ValueError('The option %s is not defined.'%name) - try: - if not os.path.exists(config_dir): - make_config_dir() - with ExclusiveFile(self.config_file_path) as f: - src = f.read() - opts = self.option_set.parse_string(src) - setattr(opts, name, val) - footer = self.option_set.get_override_section(src) - src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' - f.seek(0) - f.truncate() - if isinstance(src, unicode): - src = src.encode('utf-8') - f.write(src) - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - -class StringConfig(ConfigInterface): - ''' - A string based configuration - ''' - - def __init__(self, src, description=''): - ConfigInterface.__init__(self, description) - self.src = src - - def parse(self): - return self.option_set.parse_string(self.src) - - def set(self, name, val): - if not self.option_set.has_option(name): - raise ValueError('The option %s is not defined.'%name) - opts = self.option_set.parse_string(self.src) - setattr(opts, name, val) - footer = self.option_set.get_override_section(self.src) - self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' - -class ConfigProxy(object): - ''' - A Proxy to minimize file reads for widely used config settings - ''' - - def __init__(self, config): - self.__config = config - self.__opts = None - - @property - def defaults(self): - return self.__config.option_set.defaults - - def refresh(self): - self.__opts = self.__config.parse() - - def __getitem__(self, key): - return self.get(key) - - def __setitem__(self, key, val): - return self.set(key, val) - - def get(self, key): - if self.__opts is None: - self.refresh() - return getattr(self.__opts, key) - - def set(self, key, val): - if self.__opts is None: - self.refresh() - setattr(self.__opts, key, val) - return self.__config.set(key, val) - - def help(self, key): - return self.__config.get_option(key).help - class DynamicConfig(dict): ''' A replacement for QSettings that supports dynamic config keys. @@ -690,101 +354,6 @@ class JSONConfig(XMLConfig): -def _prefs(): - c = Config('global', 'calibre wide preferences') - c.add_opt('database_path', - default=os.path.expanduser('~/library1.db'), - help=_('Path to the database in which books are stored')) - c.add_opt('filename_pattern', default=ur'(?P.+) - (?P<author>[^_]+)', - help=_('Pattern to guess metadata from filenames')) - c.add_opt('isbndb_com_key', default='', - help=_('Access key for isbndb.com')) - c.add_opt('network_timeout', default=5, - help=_('Default timeout for network operations (seconds)')) - c.add_opt('library_path', default=None, - help=_('Path to directory in which your library of books is stored')) - c.add_opt('language', default=None, - help=_('The language in which to display the user interface')) - c.add_opt('output_format', default='EPUB', - help=_('The default output format for ebook conversions.')) - c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'LIT', 'PRC', - 'FB2', 'HTML', 'HTM', 'XHTM', 'SHTML', 'XHTML', 'ZIP', 'ODT', 'RTF', 'PDF', - 'TXT'], - help=_('Ordered list of formats to prefer for input.')) - c.add_opt('read_file_metadata', default=True, - help=_('Read metadata from files')) - c.add_opt('worker_process_priority', default='normal', - help=_('The priority of worker processes. A higher priority ' - 'means they run faster and consume more resources. ' - 'Most tasks like conversion/news download/adding books/etc. ' - 'are affected by this setting.')) - c.add_opt('swap_author_names', default=False, - help=_('Swap author first and last names when reading metadata')) - c.add_opt('add_formats_to_existing', default=False, - help=_('Add new formats to existing book records')) - c.add_opt('installation_uuid', default=None, help='Installation UUID') - c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library')) - - # these are here instead of the gui preferences because calibredb and - # calibre server can execute searches - c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) - c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) - c.add_opt('manage_device_metadata', default='manual', - help=_('How and when calibre updates metadata on the device.')) - c.add_opt('limit_search_columns', default=False, - help=_('When searching for text without using lookup ' - 'prefixes, as for example, Red instead of title:Red, ' - 'limit the columns searched to those named below.')) - c.add_opt('limit_search_columns_to', - default=['title', 'authors', 'tags', 'series', 'publisher'], - help=_('Choose columns to be searched when not using prefixes, ' - 'as for example, when searching for Redd instead of ' - 'title:Red. Enter a list of search/lookup names ' - 'separated by commas. Only takes effect if you set the option ' - 'to limit search columns above.')) - - c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') - return c - -prefs = ConfigProxy(_prefs()) -if prefs['installation_uuid'] is None: - import uuid - prefs['installation_uuid'] = str(uuid.uuid4()) - -# Read tweaks -def read_raw_tweaks(): - make_config_dir() - default_tweaks = P('default_tweaks.py', data=True, - allow_user_override=False) - tweaks_file = os.path.join(config_dir, 'tweaks.py') - if not os.path.exists(tweaks_file): - with open(tweaks_file, 'wb') as f: - f.write(default_tweaks) - with open(tweaks_file, 'rb') as f: - return default_tweaks, f.read() - -def read_tweaks(): - default_tweaks, tweaks = read_raw_tweaks() - l, g = {}, {} - try: - exec tweaks in g, l - except: - print 'Failed to load custom tweaks file' - traceback.print_exc() - dl, dg = {}, {} - exec default_tweaks in dg, dl - dl.update(l) - return dl - -def write_tweaks(raw): - make_config_dir() - tweaks_file = os.path.join(config_dir, 'tweaks.py') - with open(tweaks_file, 'wb') as f: - f.write(raw) - - -tweaks = read_tweaks() -test_eight_code = tweaks.get('test_eight_code', False) def migrate(): if hasattr(os, 'geteuid') and os.geteuid() == 0: diff --git a/src/calibre/utils/config_base.py b/src/calibre/utils/config_base.py new file mode 100644 index 0000000000..7660370353 --- /dev/null +++ b/src/calibre/utils/config_base.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' +__docformat__ = 'restructuredtext en' + +import os, re, cPickle, traceback +from functools import partial +from collections import defaultdict +from copy import deepcopy + +from calibre.utils.lock import LockError, ExclusiveFile +from calibre.constants import config_dir, CONFIG_DIR_MODE + +plugin_dir = os.path.join(config_dir, 'plugins') + +def make_config_dir(): + if not os.path.exists(plugin_dir): + os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) + +class Option(object): + + def __init__(self, name, switches=[], help='', type=None, choices=None, + check=None, group=None, default=None, action=None, metavar=None): + if choices: + type = 'choice' + + self.name = name + self.switches = switches + self.help = help.replace('%default', repr(default)) if help else None + self.type = type + if self.type is None and action is None and choices is None: + if isinstance(default, float): + self.type = 'float' + elif isinstance(default, int) and not isinstance(default, bool): + self.type = 'int' + + self.choices = choices + self.check = check + self.group = group + self.default = default + self.action = action + self.metavar = metavar + + def __eq__(self, other): + return self.name == getattr(other, 'name', other) + + def __repr__(self): + return 'Option: '+self.name + + def __str__(self): + return repr(self) + +class OptionValues(object): + + def copy(self): + return deepcopy(self) + +class OptionSet(object): + + OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', + re.DOTALL|re.IGNORECASE) + + def __init__(self, description=''): + self.description = description + self.defaults = {} + self.preferences = [] + self.group_list = [] + self.groups = {} + self.set_buffer = {} + + def has_option(self, name_or_option_object): + if name_or_option_object in self.preferences: + return True + for p in self.preferences: + if p.name == name_or_option_object: + return True + return False + + def get_option(self, name_or_option_object): + idx = self.preferences.index(name_or_option_object) + if idx > -1: + return self.preferences[idx] + for p in self.preferences: + if p.name == name_or_option_object: + return p + + def add_group(self, name, description=''): + if name in self.group_list: + raise ValueError('A group by the name %s already exists in this set'%name) + self.groups[name] = description + self.group_list.append(name) + return partial(self.add_opt, group=name) + + def update(self, other): + for name in other.groups.keys(): + self.groups[name] = other.groups[name] + if name not in self.group_list: + self.group_list.append(name) + for pref in other.preferences: + if pref in self.preferences: + self.preferences.remove(pref) + self.preferences.append(pref) + + def smart_update(self, opts1, opts2): + ''' + Updates the preference values in opts1 using only the non-default preference values in opts2. + ''' + for pref in self.preferences: + new = getattr(opts2, pref.name, pref.default) + if new != pref.default: + setattr(opts1, pref.name, new) + + def remove_opt(self, name): + if name in self.preferences: + self.preferences.remove(name) + + + def add_opt(self, name, switches=[], help=None, type=None, choices=None, + group=None, default=None, action=None, metavar=None): + ''' + Add an option to this section. + + :param name: The name of this option. Must be a valid Python identifier. + Must also be unique in this OptionSet and all its subsets. + :param switches: List of command line switches for this option + (as supplied to :module:`optparse`). If empty, this + option will not be added to the command line parser. + :param help: Help text. + :param type: Type checking of option values. Supported types are: + `None, 'choice', 'complex', 'float', 'int', 'string'`. + :param choices: List of strings or `None`. + :param group: Group this option belongs to. You must previously + have created this group with a call to :method:`add_group`. + :param default: The default value for this option. + :param action: The action to pass to optparse. Supported values are: + `None, 'count'`. For choices and boolean options, + action is automatically set correctly. + ''' + pref = Option(name, switches=switches, help=help, type=type, choices=choices, + group=group, default=default, action=action, metavar=None) + if group is not None and group not in self.groups.keys(): + raise ValueError('Group %s has not been added to this section'%group) + if pref in self.preferences: + raise ValueError('An option with the name %s already exists in this set.'%name) + self.preferences.append(pref) + self.defaults[name] = default + + def option_parser(self, user_defaults=None, usage='', gui_mode=False): + from calibre.utils.config import OptionParser + parser = OptionParser(usage, gui_mode=gui_mode) + groups = defaultdict(lambda : parser) + for group, desc in self.groups.items(): + groups[group] = parser.add_option_group(group.upper(), desc) + + for pref in self.preferences: + if not pref.switches: + continue + g = groups[pref.group] + action = pref.action + if action is None: + action = 'store' + if pref.default is True or pref.default is False: + action = 'store_' + ('false' if pref.default else 'true') + args = dict( + dest=pref.name, + help=pref.help, + metavar=pref.metavar, + type=pref.type, + choices=pref.choices, + default=getattr(user_defaults, pref.name, pref.default), + action=action, + ) + g.add_option(*pref.switches, **args) + + + return parser + + def get_override_section(self, src): + match = self.OVERRIDE_PAT.search(src) + if match: + return match.group() + return '' + + def parse_string(self, src): + options = {'cPickle':cPickle} + if src is not None: + try: + if not isinstance(src, unicode): + src = src.decode('utf-8') + exec src in options + except: + print 'Failed to parse options string:' + print repr(src) + traceback.print_exc() + opts = OptionValues() + for pref in self.preferences: + val = options.get(pref.name, pref.default) + formatter = __builtins__.get(pref.type, None) + if callable(formatter): + val = formatter(val) + setattr(opts, pref.name, val) + + return opts + + def render_group(self, name, desc, opts): + prefs = [pref for pref in self.preferences if pref.group == name] + lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] + if desc: + lines += map(lambda x: '# '+x, desc.split('\n')) + lines.append(' ') + for pref in prefs: + lines.append('# '+pref.name.replace('_', ' ')) + if pref.help: + lines += map(lambda x: '# ' + x, pref.help.split('\n')) + lines.append('%s = %s'%(pref.name, + self.serialize_opt(getattr(opts, pref.name, pref.default)))) + lines.append(' ') + return '\n'.join(lines) + + def serialize_opt(self, val): + if val is val is True or val is False or val is None or \ + isinstance(val, (int, float, long, basestring)): + return repr(val) + from PyQt4.QtCore import QString + if isinstance(val, QString): + return repr(unicode(val)) + pickle = cPickle.dumps(val, -1) + return 'cPickle.loads(%s)'%repr(pickle) + + def serialize(self, opts): + src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) + groups = [self.render_group(name, self.groups.get(name, ''), opts) \ + for name in [None] + self.group_list] + return src + '\n\n'.join(groups) + +class ConfigInterface(object): + + def __init__(self, description): + self.option_set = OptionSet(description=description) + self.add_opt = self.option_set.add_opt + self.add_group = self.option_set.add_group + self.remove_opt = self.remove = self.option_set.remove_opt + self.parse_string = self.option_set.parse_string + self.get_option = self.option_set.get_option + self.preferences = self.option_set.preferences + + def update(self, other): + self.option_set.update(other.option_set) + + def option_parser(self, usage='', gui_mode=False): + return self.option_set.option_parser(user_defaults=self.parse(), + usage=usage, gui_mode=gui_mode) + + def smart_update(self, opts1, opts2): + self.option_set.smart_update(opts1, opts2) + + +class Config(ConfigInterface): + ''' + A file based configuration. + ''' + + def __init__(self, basename, description=''): + ConfigInterface.__init__(self, description) + self.config_file_path = os.path.join(config_dir, basename+'.py') + + + def parse(self): + src = '' + if os.path.exists(self.config_file_path): + try: + with ExclusiveFile(self.config_file_path) as f: + try: + src = f.read().decode('utf-8') + except ValueError: + print "Failed to parse", self.config_file_path + traceback.print_exc() + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + return self.option_set.parse_string(src) + + def as_string(self): + if not os.path.exists(self.config_file_path): + return '' + try: + with ExclusiveFile(self.config_file_path) as f: + return f.read().decode('utf-8') + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + try: + if not os.path.exists(config_dir): + make_config_dir() + with ExclusiveFile(self.config_file_path) as f: + src = f.read() + opts = self.option_set.parse_string(src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(src) + src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + f.seek(0) + f.truncate() + if isinstance(src, unicode): + src = src.encode('utf-8') + f.write(src) + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + +class StringConfig(ConfigInterface): + ''' + A string based configuration + ''' + + def __init__(self, src, description=''): + ConfigInterface.__init__(self, description) + self.src = src + + def parse(self): + return self.option_set.parse_string(self.src) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + opts = self.option_set.parse_string(self.src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(self.src) + self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + +class ConfigProxy(object): + ''' + A Proxy to minimize file reads for widely used config settings + ''' + + def __init__(self, config): + self.__config = config + self.__opts = None + + @property + def defaults(self): + return self.__config.option_set.defaults + + def refresh(self): + self.__opts = self.__config.parse() + + def __getitem__(self, key): + return self.get(key) + + def __setitem__(self, key, val): + return self.set(key, val) + + def get(self, key): + if self.__opts is None: + self.refresh() + return getattr(self.__opts, key) + + def set(self, key, val): + if self.__opts is None: + self.refresh() + setattr(self.__opts, key, val) + return self.__config.set(key, val) + + def help(self, key): + return self.__config.get_option(key).help + + + +def _prefs(): + c = Config('global', 'calibre wide preferences') + c.add_opt('database_path', + default=os.path.expanduser('~/library1.db'), + help=_('Path to the database in which books are stored')) + c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)', + help=_('Pattern to guess metadata from filenames')) + c.add_opt('isbndb_com_key', default='', + help=_('Access key for isbndb.com')) + c.add_opt('network_timeout', default=5, + help=_('Default timeout for network operations (seconds)')) + c.add_opt('library_path', default=None, + help=_('Path to directory in which your library of books is stored')) + c.add_opt('language', default=None, + help=_('The language in which to display the user interface')) + c.add_opt('output_format', default='EPUB', + help=_('The default output format for ebook conversions.')) + c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'LIT', 'PRC', + 'FB2', 'HTML', 'HTM', 'XHTM', 'SHTML', 'XHTML', 'ZIP', 'ODT', 'RTF', 'PDF', + 'TXT'], + help=_('Ordered list of formats to prefer for input.')) + c.add_opt('read_file_metadata', default=True, + help=_('Read metadata from files')) + c.add_opt('worker_process_priority', default='normal', + help=_('The priority of worker processes. A higher priority ' + 'means they run faster and consume more resources. ' + 'Most tasks like conversion/news download/adding books/etc. ' + 'are affected by this setting.')) + c.add_opt('swap_author_names', default=False, + help=_('Swap author first and last names when reading metadata')) + c.add_opt('add_formats_to_existing', default=False, + help=_('Add new formats to existing book records')) + c.add_opt('installation_uuid', default=None, help='Installation UUID') + c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library')) + + # these are here instead of the gui preferences because calibredb and + # calibre server can execute searches + c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) + c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) + c.add_opt('manage_device_metadata', default='manual', + help=_('How and when calibre updates metadata on the device.')) + c.add_opt('limit_search_columns', default=False, + help=_('When searching for text without using lookup ' + 'prefixes, as for example, Red instead of title:Red, ' + 'limit the columns searched to those named below.')) + c.add_opt('limit_search_columns_to', + default=['title', 'authors', 'tags', 'series', 'publisher'], + help=_('Choose columns to be searched when not using prefixes, ' + 'as for example, when searching for Redd instead of ' + 'title:Red. Enter a list of search/lookup names ' + 'separated by commas. Only takes effect if you set the option ' + 'to limit search columns above.')) + + c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') + return c + +prefs = ConfigProxy(_prefs()) +if prefs['installation_uuid'] is None: + import uuid + prefs['installation_uuid'] = str(uuid.uuid4()) + +# Read tweaks +def read_raw_tweaks(): + make_config_dir() + default_tweaks = P('default_tweaks.py', data=True, + allow_user_override=False) + tweaks_file = os.path.join(config_dir, 'tweaks.py') + if not os.path.exists(tweaks_file): + with open(tweaks_file, 'wb') as f: + f.write(default_tweaks) + with open(tweaks_file, 'rb') as f: + return default_tweaks, f.read() + +def read_tweaks(): + default_tweaks, tweaks = read_raw_tweaks() + l, g = {}, {} + try: + exec tweaks in g, l + except: + import traceback + print 'Failed to load custom tweaks file' + traceback.print_exc() + dl, dg = {}, {} + exec default_tweaks in dg, dl + dl.update(l) + return dl + +def write_tweaks(raw): + make_config_dir() + tweaks_file = os.path.join(config_dir, 'tweaks.py') + with open(tweaks_file, 'wb') as f: + f.write(raw) + + +tweaks = read_tweaks() + + diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7957bd0749..aa8e4fb3a3 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -549,8 +549,22 @@ class BuiltinCapitalize(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) +class BuiltinBooksize(BuiltinFormatterFunction): + name = 'booksize' + arg_count = 0 + doc = _('booksize() -- return value of the field capitalized') + + def evaluate(self, formatter, kwargs, mi, locals): + if mi.book_size is not None: + try: + return str(mi.book_size) + except: + pass + return '' + builtin_add = BuiltinAdd() builtin_assign = BuiltinAssign() +builtin_booksize = BuiltinBooksize() builtin_capitalize = BuiltinCapitalize() builtin_cmp = BuiltinCmp() builtin_contains = BuiltinContains() diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index f17ff1b17f..d5bef449c4 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -10,7 +10,7 @@ import sys from functools import partial from calibre.constants import plugins -from calibre.utils.config import tweaks +from calibre.utils.config_base import tweaks _icu = _collator = None _locale = None diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index e3b7bfd449..ea6ce88ad6 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -17,7 +17,7 @@ from binascii import hexlify from calibre.utils.ipc.launch import Worker from calibre.utils.ipc.worker import PARALLEL_FUNCS from calibre import detect_ncpus as cpu_count -from calibre.constants import iswindows +from calibre.constants import iswindows, DEBUG from calibre.ptempfile import base_dir _counter = 0 @@ -106,13 +106,14 @@ class Server(Thread): self.add_jobs_queue, self.changed_jobs_queue = Queue(), Queue() self.kill_queue = Queue() self.waiting_jobs = [] - self.pool, self.workers = deque(), deque() + self.workers = deque() self.launched_worker_count = 0 self._worker_launch_lock = RLock() self.start() def launch_worker(self, gui=False, redirect_output=None): + start = time.time() with self._worker_launch_lock: self.launched_worker_count += 1 id = self.launched_worker_count @@ -136,6 +137,8 @@ class Server(Thread): break if isinstance(cw, basestring): raise CriticalError('Failed to launch worker process:\n'+cw) + if DEBUG: + print 'Worker Launch took:', time.time() - start return cw def do_launch(self, env, gui, redirect_output, rfile): @@ -204,13 +207,6 @@ class Server(Thread): job.duration = time.time() - job.start_time self.changed_jobs_queue.put(job) - # Start new workers - if len(self.pool) + len(self.workers) < self.pool_size: - try: - self.pool.append(self.launch_worker()) - except Exception: - pass - # Start waiting jobs sj = self.suitable_waiting_job() if sj is not None: @@ -222,7 +218,7 @@ class Server(Thread): job.killed = job.failed = True job.result = None else: - worker = self.pool.pop() + worker = self.launch_worker() worker.start_job(job) self.workers.append(worker) job.log_path = worker.log_path @@ -236,7 +232,7 @@ class Server(Thread): break def suitable_waiting_job(self): - available_workers = len(self.pool) + available_workers = self.pool_size - len(self.workers) for worker in self.workers: job = worker.job if job.core_usage == -1: @@ -302,11 +298,6 @@ class Server(Thread): worker.kill() except: pass - for worker in list(self.pool): - try: - worker.kill() - except: - pass def __enter__(self): return self diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index f676b99e43..533fd03457 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -24,7 +24,7 @@ def available_translations(): def get_lang(): 'Try to figure out what language to display the interface in' - from calibre.utils.config import prefs + from calibre.utils.config_base import prefs lang = prefs['language'] lang = os.environ.get('CALIBRE_OVERRIDE_LANG', lang) if lang is not None: diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index a50ca20fc1..3c41498107 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -16,14 +16,14 @@ methods :method:`SearchQueryParser.universal_set` and If this module is run, it will perform a series of unit tests. ''' -import sys, string, operator +import sys, operator from calibre.utils.pyparsing import CaselessKeyword, Group, Forward, \ CharsNotIn, Suppress, OneOrMore, MatchFirst, CaselessLiteral, \ Optional, NoMatch, ParseException, QuotedString from calibre.constants import preferred_encoding from calibre.utils.icu import sort_key - +from calibre import prints ''' @@ -109,7 +109,7 @@ class SearchQueryParser(object): def run_tests(parser, result, tests): failed = [] for test in tests: - print '\tTesting:', test[0], + prints('\tTesting:', test[0], end=' ') res = parser.parseString(test[0]) if list(res.get(result, None)) == test[1]: print 'OK' @@ -134,7 +134,7 @@ class SearchQueryParser(object): for l in standard_locations: location |= l location = Optional(location, default='all') - word_query = CharsNotIn(string.whitespace + '()') + word_query = CharsNotIn(u'\t\r\n\u00a0 ' + u'()') #quoted_query = Suppress('"')+CharsNotIn('"')+Suppress('"') quoted_query = QuotedString('"', escChar='\\') query = quoted_query | word_query @@ -617,7 +617,7 @@ class Tester(SearchQueryParser): def run_tests(self): failed = [] for query in self.tests.keys(): - print 'Testing query:', query, + prints('Testing query:', query, end=' ') res = self.parse(query) if res != self.tests[query]: print 'FAILED', 'Expected:', self.tests[query], 'Got:', res

All transaction (paid or otherwise) are handled between you and the store. ' - 'Calibre is not part of this process and any issues related to a purchase need to ' - 'be directed to the actual store. Be sure to double check that any books you get ' - 'will work with you device. Double check for format and ' - 'DRM ' - 'restrictions.