Sync to trunk.

This commit is contained in:
John Schember 2011-04-21 21:39:21 -04:00
commit fd5bd49c74
108 changed files with 2126 additions and 1019 deletions

View File

@ -6,12 +6,13 @@ __copyright__ = 'Copyright 2010 Starson17'
www.arcamax.com www.arcamax.com
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class Arcamax(BasicNewsRecipe): class Arcamax(BasicNewsRecipe):
title = 'Arcamax' title = 'Arcamax'
__author__ = 'Starson17' __author__ = 'Starson17'
__version__ = '1.03' __version__ = '1.04'
__date__ = '25 November 2010' __date__ = '18 April 2011'
description = u'Family Friendly Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.' description = u'Family Friendly Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.'
category = 'news, comics' category = 'news, comics'
language = 'en' language = 'en'
@ -30,8 +31,15 @@ class Arcamax(BasicNewsRecipe):
, 'language' : language , '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): def parse_index(self):
feeds = [] feeds = []
@ -71,7 +79,6 @@ class Arcamax(BasicNewsRecipe):
#(u"Rugrats", u"http://www.arcamax.com/rugrats"), #(u"Rugrats", u"http://www.arcamax.com/rugrats"),
(u"Speed Bump", u"http://www.arcamax.com/speedbump"), (u"Speed Bump", u"http://www.arcamax.com/speedbump"),
(u"Wizard of Id", u"http://www.arcamax.com/wizardofid"), (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"), (u"Zits", u"http://www.arcamax.com/zits"),
]: ]:
articles = self.make_links(url) articles = self.make_links(url)
@ -86,24 +93,37 @@ class Arcamax(BasicNewsRecipe):
for page in pages: for page in pages:
page_soup = self.index_to_soup(url) page_soup = self.index_to_soup(url)
if page_soup: 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 page_url = url
prev_page_url = 'http://www.arcamax.com' + page_soup.find('a', attrs={'class':'next'}, text='Previous').parent['href'] # orig prev_page_url = 'http://www.arcamax.com' + page_soup.find('a', attrs={'class':'prev'}, text='Previous').parent['href']
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':''}) 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 url = prev_page_url
current_articles.reverse() current_articles.reverse()
return current_articles return current_articles
def preprocess_html(self, soup): def preprocess_html(self, soup):
main_comic = soup.find('p',attrs={'class':'m0'}) for img_tag in soup.findAll('img'):
if main_comic.a['target'] == '_blank': parent_tag = img_tag.parent
main_comic.a.img['id'] = 'main_comic' 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 return soup
extra_css = ''' extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} 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;} 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;} p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;} body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
''' '''

59
recipes/babyonline.recipe Normal file
View File

@ -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)

View File

@ -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'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'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'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 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'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'), (u'Confidential News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_confidential_252.xml'),

View File

@ -0,0 +1,83 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2011, Nikolas Mangold <nmangold at gmail.com>'
'''
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'<p>&#9670;</p>', re.DOTALL|re.IGNORECASE), lambda match: '<hr>'),
]
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;

View File

@ -14,14 +14,14 @@ class EcuisineRo(BasicNewsRecipe):
__author__ = u'Silviu Cotoar\u0103' __author__ = u'Silviu Cotoar\u0103'
description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti' description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti'
publisher = 'eCuisine' publisher = 'eCuisine'
oldest_article = 5 oldest_article = 50
language = 'ro' language = 'ro'
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
category = 'Ziare,Retete,Bucatarie' category = 'Ziare,Retete,Bucatarie'
encoding = 'utf-8' encoding = 'utf-8'
cover_url = '' cover_url = 'http://www.ecuisine.ro/sites/all/themes/ecuisine/images/logo.gif'
conversion_options = { conversion_options = {
'comments' : description 'comments' : description
@ -31,8 +31,8 @@ class EcuisineRo(BasicNewsRecipe):
} }
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'class':'page-title'}) dict(name='h1', attrs={'id':'page-title'})
, dict(name='div', attrs={'class':'content clearfix'}) , dict(name='div', attrs={'class':'field-item even'})
] ]
remove_tags = [ remove_tags = [

View File

@ -31,8 +31,8 @@ class EgirlRo(BasicNewsRecipe):
} }
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'id':'title_art'}) dict(name='div', attrs={'id':'content_art'})
, dict(name='div', attrs={'class':'content_style'}) , dict(name='div', attrs={'class':'content_articol'})
] ]
feeds = [ feeds = [

View File

@ -1,4 +1,3 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Handelsblatt(BasicNewsRecipe): class Handelsblatt(BasicNewsRecipe):
@ -7,14 +6,11 @@ class Handelsblatt(BasicNewsRecipe):
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True 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' language = 'de'
# keep_only_tags = []
keep_only_tags = (dict(name = 'div', attrs = {'class': ['hcf-detail-abstract hcf-teaser ajaxify','hcf-detail','hcf-author-wrapper']})) remove_tags_before = dict(attrs={'class':'hcf-overline'})
# keep_only_tags.append(dict(name = 'div', attrs = {'id': 'fullText'})) remove_tags_after = dict(attrs={'class':'hcf-footer'})
remove_tags = [dict(name='img', attrs = {'src': 'http://www.handelsblatt.com/images/icon/loading.gif'})
,dict(name='ul' , attrs={'class':['hcf-detail-tools']})
]
feeds = [ feeds = [
(u'Handelsblatt Exklusiv',u'http://www.handelsblatt.com/rss/exklusiv'), (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 Magazin',u'http://www.handelsblatt.com/rss/magazin/'),
(u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs') (u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs')
] ]
extra_css = ''' extra_css = '''
.hcf-headline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
.hcf-overline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.hcf-exclusive {font-family:Arial,Helvetica,sans-serif; font-style:italic;font-weight:bold; margin-right:5pt;} p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;} body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
.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;}
''' '''
def print_version(self, url):
url = url.split('/')
url[-1] = 'v_detail_tab_print,'+url[-1]
url = '/'.join(url)
return url

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

View File

@ -1,71 +1,65 @@
#!/usr/bin/env python __author__ = 'Marco Saraceno'
__license__ = 'GPL v3' __copyright__ = '2010, Marco Saraceno <marcosaraceno at gmail.com>'
__author__ = 'Lorenzo Vigentini & Edwin van Maastrigt' description = 'Italian daily newspaper - v 1.1 (Mar14,2011)'
__copyright__ = '2009, Lorenzo Vigentini <l.vigentini at gmail.com> and Edwin van Maastrigt <evanmaastrigt at gmail.com>'
__description__ = 'Financial news daily paper - v1.02 (30, January 2010)'
''' '''
http://www.ilsole24ore.com/ http://www.ilsole24ore.com
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class IlSole24Ore(BasicNewsRecipe):
__author__ = 'Marco Saraceno'
description = 'Italian financial daily newspaper'
class ilsole24Ore(BasicNewsRecipe): cover_url = 'http://www.shopping24.ilsole24ore.com/ProductRelated/rds/img/logo_sole.gif'
author = 'Lorenzo Vigentini & Edwin van Maastrigt' title = u'Il Sole 24 Ore'
description = 'Financial news daily paper' publisher = 'Gruppo editoriale GRUPPO 24ORE'
category = 'News, politics, culture, economy, financial, Italian'
cover_url = 'http://www.ilsole24ore.com/img2007/print_header.gif'
title = u'il Sole 24 Ore New'
publisher = 'italiaNews'
category = 'News, finance, economy, politics'
language = 'it' language = 'it'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 50 max_articles_per_feed = 100
use_embedded_content = False 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): 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): def print_version(self, url):
link, sep, params = url.rpartition('?') return url.replace('.shtml', '_PRN.shtml')
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;}
'''

View File

@ -1,4 +1,3 @@
import string
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Newsweek(BasicNewsRecipe): class Newsweek(BasicNewsRecipe):
@ -11,7 +10,6 @@ class Newsweek(BasicNewsRecipe):
no_stylesheets = True no_stylesheets = True
BASE_URL = 'http://www.newsweek.com' BASE_URL = 'http://www.newsweek.com'
INDEX = BASE_URL+'/topics.html'
keep_only_tags = dict(name='article', attrs={'class':'article-text'}) keep_only_tags = dict(name='article', attrs={'class':'article-text'})
remove_tags = [dict(attrs={'data-dartad':True})] remove_tags = [dict(attrs={'data-dartad':True})]
@ -23,11 +21,14 @@ class Newsweek(BasicNewsRecipe):
return soup return soup
def newsweek_sections(self): def newsweek_sections(self):
soup = self.index_to_soup(self.INDEX) return [
for a in soup.findAll('a', title='Primary tag', href=True): ('Nation', 'http://www.newsweek.com/tag/nation.html'),
yield (string.capitalize(self.tag_to_string(a)), ('Society', 'http://www.newsweek.com/tag/society.html'),
self.BASE_URL+a['href']) ('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): def newsweek_parse_section_page(self, soup):
for article in soup.findAll('article', about=True, for article in soup.findAll('article', about=True,

View File

@ -8,23 +8,36 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net> edited by Huan T'
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Slashdot(BasicNewsRecipe): class Slashdot(BasicNewsRecipe):
title = u'Slashdot.org' title = u'Slashdot.org'
description = '''Tech news. WARNING: This recipe downloads a lot description = '''Tech news. WARNING: This recipe downloads a lot
of content and may result in your IP being banned from slashdot.org''' of content and may result in your IP being banned from slashdot.org'''
oldest_article = 7 oldest_article = 7
simultaneous_downloads = 1 simultaneous_downloads = 1
delay = 3 delay = 3
max_articles_per_feed = 100 max_articles_per_feed = 100
language = 'en' language = 'en'
__author__ = 'floweros edited by Huan T' __author__ = 'floweros edited by Huan T'
no_stylesheets = True no_stylesheets = True
# keep_only_tags = [ keep_only_tags = [
# dict(name='div',attrs={'class':'article'}), dict(name='div',attrs={'id':'article'}),
# dict(name='div',attrs={'class':'commentTop'}), 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'Slashdot',
u'http://rss.slashdot.org/Slashdot/slashdot'), u'http://rss.slashdot.org/Slashdot/slashdot'),
(u'/. IT', (u'/. IT',
@ -37,5 +50,3 @@ class Slashdot(BasicNewsRecipe):
u'http://rss.slashdot.org/Slashdot/slashdotYourRightsOnline') u'http://rss.slashdot.org/Slashdot/slashdotYourRightsOnline')
] ]
def get_article_url(self, article):
return article.get('feedburner_origlink', None)

View File

@ -37,10 +37,12 @@ class TabuRo(BasicNewsRecipe):
] ]
remove_tags = [ remove_tags = [
dict(name='div', attrs={'class':'asemanatoare'}) dict(name='div', attrs={'class':'asemanatoare'}),
dict(name='div', attrs={'class':'social'})
] ]
remove_tags_after = [ remove_tags_after = [
dict(name='div', attrs={'class':'social'}),
dict(name='div', attrs={'id':'comments'}), dict(name='div', attrs={'id':'comments'}),
dict(name='div', attrs={'class':'asemanatoare'}) dict(name='div', attrs={'class':'asemanatoare'})
] ]

View File

@ -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/')]

View File

@ -48,7 +48,7 @@ authors_completer_append_separator = False
# When this tweak is changed, the author_sort values stored with each author # 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, # 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'. # 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 #: Use author sort in Tag Browser
# Set which author field to display in the tags pane (the list of authors, # Set which author field to display in the tags pane (the list of authors,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

@ -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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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" "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"
} }

View File

@ -15,9 +15,9 @@ from setup import prints, get_warnings
def check_version_info(): def check_version_info():
vi = sys.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 None
return 'calibre requires python >= 2.6' return 'calibre requires python >= 2.7 and < 3'
def option_parser(): def option_parser():
parser = optparse.OptionParser() parser = optparse.OptionParser()

View File

@ -24,8 +24,10 @@ def initialize_constants():
global __version__, __appname__, modules, functions, basenames, scripts global __version__, __appname__, modules, functions, basenames, scripts
src = open('src/calibre/constants.py', 'rb').read() src = open('src/calibre/constants.py', 'rb').read()
__version__ = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) nv = re.search(r'numeric_version\s+=\s+\((\d+), (\d+), (\d+)\)', src)
__appname__ = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) __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).\ epsrc = re.compile(r'entry_points = (\{.*?\})', re.DOTALL).\
search(open('src/calibre/linux.py', 'rb').read()).group(1) search(open('src/calibre/linux.py', 'rb').read()).group(1)
entry_points = eval(epsrc, {'__appname__': __appname__}) entry_points = eval(epsrc, {'__appname__': __appname__})

View File

@ -13,7 +13,8 @@ from setup import Command, modules, functions, basenames, __version__, \
from setup.build_environment import msvc, MT, RC from setup.build_environment import msvc, MT, RC
from setup.installer.windows.wix import WixMixIn 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'] QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns']
LIBUSB_DIR = 'C:\\libusb' LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll' LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
@ -108,6 +109,8 @@ class Win32Freeze(Command, WixMixIn):
self.dll_dir = self.j(self.base, 'DLLs') self.dll_dir = self.j(self.base, 'DLLs')
shutil.copytree(r'C:\Python%s\DLLs'%self.py_ver, self.dll_dir, shutil.copytree(r'C:\Python%s\DLLs'%self.py_ver, self.dll_dir,
ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*')) 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: for x in QT_DLLS:
x += '4.dll' x += '4.dll'
if not x.startswith('phonon'): x = 'Qt'+x if not x.startswith('phonon'): x = 'Qt'+x

View File

@ -53,12 +53,25 @@ SQLite
Put sqlite3*.h from the sqlite windows amlgamation in ~/sw/include 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 Qt
-------- --------
Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make:: 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 SIP
----- -----

View File

@ -3,11 +3,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import uuid, sys, os, re, logging, time, random, \ import sys, os, re, time, random, __builtin__, warnings
__builtin__, warnings, multiprocessing
from contextlib import closing
from urllib import getproxies
from urllib2 import unquote as urllib2_unquote
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None) __builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
from htmlentitydefs import name2codepoint from htmlentitydefs import name2codepoint
from math import floor from math import floor
@ -16,25 +12,51 @@ from functools import partial
warnings.simplefilter('ignore', DeprecationWarning) warnings.simplefilter('ignore', DeprecationWarning)
from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \ from calibre.constants import (iswindows, isosx, islinux, isfreebsd, isfrozen,
terminal_controller, preferred_encoding, \ preferred_encoding, __appname__, __version__, __author__,
__appname__, __version__, __author__, \ win32event, win32api, winerror, fcntl,
win32event, win32api, winerror, fcntl, \ filesystem_encoding, plugins, config_dir)
filesystem_encoding, plugins, config_dir from calibre.startup import winutil, winutilerror
from calibre.startup import winutil, winutilerror, guess_type
if islinux and not getattr(sys, 'frozen', False): if False and islinux and not getattr(sys, 'frozen', False):
# Imported before PyQt4 to workaround PyQt4 util-linux conflict on gentoo # 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() uuid.uuid4()
if False: if False:
# Prevent pyflakes from complaining # Prevent pyflakes from complaining
winutil, winutilerror, __appname__, islinux, __version__ winutil, winutilerror, __appname__, islinux, __version__
fcntl, win32event, isfrozen, __author__, terminal_controller fcntl, win32event, isfrozen, __author__
winerror, win32api, isfreebsd, guess_type winerror, win32api, isfreebsd
import cssutils _mt_inited = False
cssutils.log.setLevel(logging.WARN) 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'): def to_unicode(raw, encoding='utf-8', errors='strict'):
if isinstance(raw, unicode): if isinstance(raw, unicode):
@ -182,6 +204,7 @@ class CommandLineError(Exception):
pass pass
def setup_cli_handlers(logger, level): def setup_cli_handlers(logger, level):
import logging
if os.environ.get('CALIBRE_WORKER', None) is not None and logger.handlers: if os.environ.get('CALIBRE_WORKER', None) is not None and logger.handlers:
return return
logger.setLevel(level) logger.setLevel(level)
@ -243,6 +266,7 @@ def extract(path, dir):
extractor(path, dir) extractor(path, dir)
def get_proxies(debug=True): def get_proxies(debug=True):
from urllib import getproxies
proxies = getproxies() proxies = getproxies()
for key, proxy in list(proxies.items()): for key, proxy in list(proxies.items()):
if not proxy or '..' in proxy: if not proxy or '..' in proxy:
@ -386,6 +410,7 @@ class StreamReadWrapper(object):
def detect_ncpus(): def detect_ncpus():
"""Detects the number of effective CPUs in the system""" """Detects the number of effective CPUs in the system"""
import multiprocessing
ans = -1 ans = -1
try: try:
ans = multiprocessing.cpu_count() 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 Get a local filename for a URL using the content disposition header
''' '''
from contextlib import closing
from urllib2 import unquote as urllib2_unquote
filename = '' filename = ''
br = browser() br = browser()
@ -679,4 +707,3 @@ main()
ipshell() ipshell()
sys.argv = old_argv sys.argv = old_argv

View File

@ -1,28 +1,32 @@
from future_builtins import map
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = u'calibre'
__version__ = '0.7.56' numeric_version = (0, 7, 56)
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
import re, importlib
_ver = __version__.split('.')
_ver = [int(re.search(r'(\d+)', x).group(1)) for x in _ver]
numeric_version = tuple(_ver)
''' '''
Various run time constants. Various run time constants.
''' '''
import sys, locale, codecs, os import sys, locale, codecs, os, importlib, collections
from calibre.utils.terminfo import TerminalController
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() _plat = sys.platform.lower()
isosx = 'darwin' in sys.platform.lower() iswindows = 'win32' in _plat or 'win64' in _plat
isnewosx = isosx and getattr(sys, 'new_app_bundle', False) isosx = 'darwin' in _plat
isfreebsd = 'freebsd' in sys.platform.lower() isnewosx = isosx and getattr(sys, 'new_app_bundle', False)
isfreebsd = 'freebsd' in _plat
islinux = not(iswindows or isosx or isfreebsd) islinux = not(iswindows or isosx or isfreebsd)
isfrozen = hasattr(sys, 'frozen') isfrozen = hasattr(sys, 'frozen')
isunix = isosx or islinux isunix = isosx or islinux
@ -41,6 +45,7 @@ fcntl = None if iswindows else importlib.import_module('fcntl')
filesystem_encoding = sys.getfilesystemencoding() filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8' if filesystem_encoding is None: filesystem_encoding = 'utf-8'
DEBUG = False DEBUG = False
def debug(): def debug():
@ -48,15 +53,12 @@ def debug():
DEBUG = True DEBUG = True
# plugins {{{ # 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', 'pictureflow',
'lzx', 'lzx',
'msdes', 'msdes',
@ -70,19 +72,44 @@ if plugins is None:
'chm_extra', 'chm_extra',
'icu', 'icu',
'speedup', 'speedup',
] + \ ]
(['winutil'] if iswindows else []) + \ if iswindows:
(['usbobserver'] if isosx else []): plugins.append('winutil')
try: if isosx:
p, err = importlib.import_module(plugin), '' plugins.append('usbobserver')
except Exception as err: self.plugins = frozenset(plugins)
p = None
err = str(err)
plugins[plugin] = (p, err)
sys.path.remove(plugin_path)
return 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 {{{ # config_dir {{{

View File

@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
from calibre.constants import numeric_version from calibre.constants import numeric_version
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf 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 from calibre.utils.config import test_eight_code
# To archive plugins {{{ # To archive plugins {{{
@ -98,6 +97,8 @@ class TXT2TXTZ(FileTypePlugin):
on_import = True on_import = True
def _get_image_references(self, txt, base_dir): def _get_image_references(self, txt, base_dir):
from calibre.ebooks.oeb.base import OEB_IMAGES
images = [] images = []
# Textile # Textile
@ -626,8 +627,9 @@ if test_eight_code:
from calibre.ebooks.metadata.sources.amazon import Amazon from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
from calibre.ebooks.metadata.sources.isbndb import ISBNDB 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: else:

View File

@ -22,6 +22,11 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
from calibre.ebooks.epub.fix import ePubFixer from calibre.ebooks.epub.fix import ePubFixer
from calibre.ebooks.metadata.sources.base import Source from calibre.ebooks.metadata.sources.base import Source
builtin_names = frozenset([p.name for p in builtin_plugins])
class NameConflict(ValueError):
pass
def _config(): def _config():
c = Config('customize') c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins')) 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): def add_plugin(path_to_zip_file):
make_config_dir() make_config_dir()
plugin = load_plugin(path_to_zip_file) 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) plugin = initialize_plugin(plugin, path_to_zip_file)
plugins = config['plugins'] plugins = config['plugins']
zfp = os.path.join(plugin_dir, plugin.name+'.zip') zfp = os.path.join(plugin_dir, plugin.name+'.zip')
@ -506,7 +514,11 @@ def initialize_plugin(plugin, path_to_zip_file):
def initialize_plugins(): def initialize_plugins():
global _initialized_plugins global _initialized_plugins
_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:
try: try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp

View File

@ -106,7 +106,7 @@ def migrate(old, new):
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.utils.terminfo import ProgressBar from calibre.utils.terminfo import ProgressBar
from calibre import terminal_controller from calibre.constants import terminal_controller
class Dummy(ProgressBar): class Dummy(ProgressBar):
def setLabelText(self, x): pass def setLabelText(self, x): pass
def setAutoReset(self, y): pass def setAutoReset(self, y): pass
@ -119,7 +119,7 @@ def migrate(old, new):
db = LibraryDatabase(old) db = LibraryDatabase(old)
db2 = LibraryDatabase2(new) 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) prefs['library_path'] = os.path.abspath(new)
print 'Database migrated to', os.path.abspath(new) print 'Database migrated to', os.path.abspath(new)

View File

@ -108,10 +108,10 @@ class ANDROID(USBMS):
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2', '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', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7'] 'A70S', 'A101IT', '7', 'INCREDIBLE']
OSX_MAIN_MEM = 'Android Device Main Memory' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -8,7 +8,7 @@ manner.
import sys, os, re import sys, os, re
from threading import RLock 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 osx_scanner = win_scanner = linux_scanner = None

View File

@ -7,7 +7,7 @@ Code for the conversion of ebook formats and the reading of metadata
from various formats. from various formats.
''' '''
import traceback, os import traceback, os, re
from calibre import CurrentDir from calibre import CurrentDir
class ConversionError(Exception): class ConversionError(Exception):
@ -169,3 +169,42 @@ def calibre_cover(title, author_string, series_string=None,
lines.append(TextLine(series_string, author_size)) lines.append(TextLine(series_string, author_size))
return create_cover_page(lines, I('library.png'), output_format='jpg') 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

View File

@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>,' \
' and Alex Bramley <a.bramley at gmail.com>.' ' and Alex Bramley <a.bramley at gmail.com>.'
import os, re 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.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
from calibre.constants import iswindows, filesystem_encoding from calibre.constants import iswindows, filesystem_encoding
from calibre.utils.chm.chm import CHMFile from calibre.utils.chm.chm import CHMFile

View File

@ -14,7 +14,8 @@ from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.date import parse_date from calibre.utils.date import parse_date
from calibre.utils.zipfile import ZipFile 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__ from calibre.constants import __version__
DEBUG_README=u''' DEBUG_README=u'''
@ -875,6 +876,9 @@ OptionRecommendation(name='sr3_replace',
if self.opts.verbose: if self.opts.verbose:
self.log.filter_level = self.log.DEBUG self.log.filter_level = self.log.DEBUG
self.flush() 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: if self.opts.debug_pipeline is not None:
self.opts.verbose = max(self.opts.verbose, 4) self.opts.verbose = max(self.opts.verbose, 4)

View File

@ -399,7 +399,7 @@ class HTMLPreProcessor(object):
(re.compile(u'˙\s*(<br.*?>)*\s*Z', re.UNICODE), lambda match: u'Ż'), (re.compile(u'˙\s*(<br.*?>)*\s*Z', re.UNICODE), lambda match: u'Ż'),
# If pdf printed from a browser then the header/footer has a reliable pattern # If pdf printed from a browser then the header/footer has a reliable pattern
(re.compile(r'((?<=</a>)\s*file:////?[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''), (re.compile(r'((?<=</a>)\s*file:/{2,4}[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
# Center separator lines # Center separator lines
(re.compile(u'<br>\s*(?P<break>([*#•✦=]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'), (re.compile(u'<br>\s*(?P<break>([*#•✦=]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),

View File

@ -764,6 +764,7 @@ class HeuristicProcessor(object):
# Multiple sequential blank paragraphs are merged with appropriate margins # 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 non-blank scene breaks exist they are center aligned and styled with appropriate margins.
if getattr(self.extra_opts, 'format_scene_breaks', False): if getattr(self.extra_opts, 'format_scene_breaks', False):
html = re.sub('(?i)<div[^>]*>\s*<br(\s?/)?>\s*</div>', '<p></p>', html)
html = self.detect_whitespace(html) html = self.detect_whitespace(html)
html = self.detect_soft_breaks(html) html = self.detect_soft_breaks(html)
blanks_count = len(self.any_multi_blank.findall(html)) blanks_count = len(self.any_multi_blank.findall(html))

View File

@ -10,7 +10,6 @@ Transform OEB content into FB2 markup
from base64 import b64encode from base64 import b64encode
from datetime import datetime from datetime import datetime
from mimetypes import types_map
import re import re
import uuid import uuid
@ -18,9 +17,6 @@ from lxml import etree
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.constants import __appname__, __version__ 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 from calibre.utils.magick import Image
class FB2MLizer(object): class FB2MLizer(object):
@ -100,6 +96,7 @@ class FB2MLizer(object):
return text return text
def fb2_header(self): def fb2_header(self):
from calibre.ebooks.oeb.base import OPF
metadata = {} metadata = {}
metadata['title'] = self.oeb_book.metadata.title[0].value metadata['title'] = self.oeb_book.metadata.title[0].value
metadata['appname'] = __appname__ metadata['appname'] = __appname__
@ -180,6 +177,8 @@ class FB2MLizer(object):
return u'</FictionBook>' return u'</FictionBook>'
def get_cover(self): def get_cover(self):
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
cover_href = None cover_href = None
# Get the raster cover if it's available. # Get the raster cover if it's available.
@ -213,6 +212,8 @@ class FB2MLizer(object):
return u'' return u''
def get_text(self): def get_text(self):
from calibre.ebooks.oeb.base import XHTML
from calibre.ebooks.oeb.stylizer import Stylizer
text = ['<body>'] text = ['<body>']
# Create main section if there are no others to create # 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. 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 = [] images = []
for item in self.oeb_book.manifest: for item in self.oeb_book.manifest:
# Don't write the image if it's not referenced in the document's text. # Don't write the image if it's not referenced in the document's text.
@ -255,7 +258,7 @@ class FB2MLizer(object):
continue continue
if item.media_type in OEB_RASTER_IMAGES: if item.media_type in OEB_RASTER_IMAGES:
try: 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 = Image()
im.load(item.data) im.load(item.data)
im.set_compression_quality(70) im.set_compression_quality(70)
@ -344,6 +347,8 @@ class FB2MLizer(object):
@return: List of string representing the XHTML converted to FB2 markup. @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. # 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: if not isinstance(elem_tree.tag, basestring) or namespace(elem_tree.tag) != XHTML_NS:
return [] return []

View File

@ -315,7 +315,8 @@ class HTMLInput(InputFormatPlugin):
from calibre import guess_type from calibre import guess_type
from calibre.ebooks.oeb.transforms.metadata import \ from calibre.ebooks.oeb.transforms.metadata import \
meta_info_to_oeb_metadata meta_info_to_oeb_metadata
import cssutils import cssutils, logging
cssutils.log.setLevel(logging.WARN)
self.OEB_STYLES = OEB_STYLES self.OEB_STYLES = OEB_STYLES
oeb = create_oebbook(log, None, opts, self, oeb = create_oebbook(log, None, opts, self,
encoding=opts.input_encoding, populate=False) encoding=opts.input_encoding, populate=False)

View File

@ -4,7 +4,6 @@ __copyright__ = '2010, Fabian Grassl <fg@jusmeum.de>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from calibre.ebooks.oeb.base import namespace, barename, DC11_NS
class EasyMeta(object): class EasyMeta(object):
@ -12,6 +11,7 @@ class EasyMeta(object):
self.meta = meta self.meta = meta
def __iter__(self): def __iter__(self):
from calibre.ebooks.oeb.base import namespace, barename, DC11_NS
meta = self.meta meta = self.meta
for item_name in meta.items: for item_name in meta.items:
for item in meta[item_name]: for item in meta[item_name]:

View File

@ -12,7 +12,6 @@ from os.path import dirname, abspath, relpath, exists, basename
from lxml import etree from lxml import etree
from templite import Templite from templite import Templite
from calibre.ebooks.oeb.base import element
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
from calibre import CurrentDir from calibre import CurrentDir
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
@ -51,6 +50,7 @@ class HTMLOutput(OutputFormatPlugin):
''' '''
Generate table of contents Generate table of contents
''' '''
from calibre.ebooks.oeb.base import element
with CurrentDir(output_dir): with CurrentDir(output_dir):
def build_node(current_node, parent=None): def build_node(current_node, parent=None):
if parent is None: if parent is None:

View File

@ -12,7 +12,6 @@ from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
@ -42,6 +41,8 @@ class HTMLZOutput(OutputFormatPlugin):
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME
# HTML # HTML
if opts.htmlz_css_type == 'inline': if opts.htmlz_css_type == 'inline':
from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer

View File

@ -6,11 +6,11 @@ __docformat__ = 'restructuredtext en'
""" """
Provides abstraction for metadata reading.writing from a variety of ebook formats. 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 urllib import unquote, quote
from urlparse import urlparse from urlparse import urlparse
from calibre import relpath from calibre import relpath, guess_type
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -118,7 +118,7 @@ class Resource(object):
self.path = None self.path = None
self.fragment = '' self.fragment = ''
try: try:
self.mime_type = mimetypes.guess_type(href_or_path)[0] self.mime_type = guess_type(href_or_path)[0]
except: except:
self.mime_type = None self.mime_type = None
if self.mime_type is None: if self.mime_type is None:

View File

@ -592,7 +592,7 @@ class Metadata(object):
elif datatype == 'bool': elif datatype == 'bool':
res = _('Yes') if res else _('No') res = _('Yes') if res else _('No')
elif datatype == 'rating': elif datatype == 'rating':
res = res/2 res = res/2.0
return (name, unicode(res), orig_res, cmeta) return (name, unicode(res), orig_res, cmeta)
# convert top-level ids into their value # convert top-level ids into their value
@ -625,6 +625,8 @@ class Metadata(object):
res = res + ' [%s]'%self.format_series_index() res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime': elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) 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 (name, unicode(res), orig_res, fmeta)
return (None, None, None, None) return (None, None, None, None)

View File

@ -5,11 +5,12 @@ __copyright__ = '2008, Anatoly Shipitsin <norguhtar at gmail.com>'
'''Read meta information from fb2 files''' '''Read meta information from fb2 files'''
import mimetypes, os import os
from base64 import b64decode from base64 import b64decode
from lxml import etree from lxml import etree
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre import guess_all_extensions
XLINK_NS = 'http://www.w3.org/1999/xlink' XLINK_NS = 'http://www.w3.org/1999/xlink'
def XLINK(name): def XLINK(name):
@ -71,7 +72,7 @@ def get_metadata(stream):
binary = XPath('//fb2:binary[@id="%s"]'%id)(root) binary = XPath('//fb2:binary[@id="%s"]'%id)(root)
if binary: if binary:
mt = binary[0].get('content-type', 'image/jpeg') mt = binary[0].get('content-type', 'image/jpeg')
exts = mimetypes.guess_all_extensions(mt) exts = guess_all_extensions(mt)
if not exts: if not exts:
exts = ['.jpg'] exts = ['.jpg']
cdata = (exts[0][1:], b64decode(tostring(binary[0]))) cdata = (exts[0][1:], b64decode(tostring(binary[0])))

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
lxml based OPF parser. 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 urllib import unquote
from urlparse import urlparse 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.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_date, isoformat from calibre.utils.date import parse_date, isoformat
from calibre.utils.localization import get_lang 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 from calibre.utils.cleantext import clean_ascii_chars
class Resource(object): # {{{ class Resource(object): # {{{
@ -42,7 +42,7 @@ class Resource(object): # {{{
self.path = None self.path = None
self.fragment = '' self.fragment = ''
try: try:
self.mime_type = mimetypes.guess_type(href_or_path)[0] self.mime_type = guess_type(href_or_path)[0]
except: except:
self.mime_type = None self.mime_type = None
if self.mime_type is 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 t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
for item in self.guide: for item in self.guide:
if item.type.lower() == t: 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) return property(fget=fget, fset=fset)

View File

@ -274,26 +274,34 @@ class Source(Plugin):
if authors: if authors:
# Leave ' in there for Irish names # 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: if only_first_author:
authors = authors[:1] authors = authors[:1]
for au in authors: for au in authors:
au = replace_pat.sub(' ', au)
parts = au.split() parts = au.split()
if ',' in au: if ',' in au:
# au probably in ln, fn form # au probably in ln, fn form
parts = parts[1:] + parts[:1] parts = parts[1:] + parts[:1]
for tok in parts: 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', ): if len(tok) > 2 and tok.lower() not in ('von', ):
yield tok 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. 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: 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 title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
[ [
# Remove things like: (2010) (Omnibus) etc. # Remove things like: (2010) (Omnibus) etc.
@ -305,17 +313,20 @@ class Source(Plugin):
(r'(\d+),(\d+)', r'\1\2'), (r'(\d+),(\d+)', r'\1\2'),
# Remove hyphens only if they have whitespace before them # Remove hyphens only if they have whitespace before them
(r'(\s-)', ' '), (r'(\s-)', ' '),
# Remove single quotes # Remove single quotes not followed by 's'
(r"'", ''), (r"'(?!s)", ''),
# Replace other special chars with a space # Replace other special chars with a space
(r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ') (r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ')
]] ]]
for pat, repl in title_patterns: for pat, repl in title_patterns:
title = pat.sub(repl, title) title = pat.sub(repl, title)
tokens = title.split() tokens = title.split()
for token in tokens: for token in tokens:
token = token.strip() 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 yield token
def split_jobs(self, jobs, num): def split_jobs(self, jobs, num):
@ -363,7 +374,12 @@ class Source(Plugin):
def get_book_url(self, identifiers): def get_book_url(self, identifiers):
''' '''
Return the URL for the book identified by identifiers at this source. 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 return None

View File

@ -382,7 +382,11 @@ def identify(log, abort, # {{{
log(plog) log(plog)
log('\n'+'*'*80) log('\n'+'*'*80)
dummy = Metadata(_('Unknown'))
for i, result in enumerate(presults): 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.relevance_in_source = i
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
and plugin.get_cached_cover_url(result.identifiers) is not and plugin.get_cached_cover_url(result.identifiers) is not
@ -433,7 +437,7 @@ def urls_from_identifiers(identifiers): # {{{
pass pass
isbn = identifiers.get('isbn', None) isbn = identifiers.get('isbn', None)
if isbn: if isbn:
ans.append(('ISBN', ans.append((isbn,
'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn)) 'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn))
return ans return ans
# }}} # }}}
@ -444,13 +448,18 @@ if __name__ == '__main__': # tests {{{
from calibre.ebooks.metadata.sources.test import (test_identify, from calibre.ebooks.metadata.sources.test import (test_identify,
title_test, authors_test) title_test, authors_test)
tests = [ 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 ( # An e-book ISBN not on Amazon, one of the authors is
# unknown to Amazon # unknown to Amazon
{'identifiers':{'isbn': '9780307459671'}, {'identifiers':{'isbn': '9780307459671'},
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']}, 'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
[title_test('The Invisible Gorilla', [title_test('The Invisible Gorilla', exact=True)]
exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])]
), ),

View File

@ -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<img>(Ima?g(eType-)?))200', '\g<img>100', 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<displayrecords>\d+).*?"iTotalRecords":(?P<totalrecords>\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<content>.*?)\]\].*', '[[\g<content>]]', 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'])]
),
])

View File

@ -67,6 +67,23 @@ def authors_test(authors):
return test 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): def init_test(tdir_name):
tdir = tempfile.gettempdir() tdir = tempfile.gettempdir()
lf = os.path.join(tdir, tdir_name.replace(' ', '')+'_identify_test.txt') lf = os.path.join(tdir, tdir_name.replace(' ', '')+'_identify_test.txt')

View File

@ -20,7 +20,7 @@ from calibre.utils.filenames import ascii_filename
from calibre.utils.date import parse_date from calibre.utils.date import parse_date
from calibre.utils.cleantext import clean_ascii_chars from calibre.utils.cleantext import clean_ascii_chars
from calibre.ptempfile import TemporaryDirectory 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.chardet import ENCODING_PATS
from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.huffcdic import HuffReader from calibre.ebooks.mobi.huffcdic import HuffReader
@ -258,6 +258,8 @@ class MobiReader(object):
} }
''') ''')
self.tag_css_rules = {} self.tag_css_rules = {}
self.left_margins = {}
self.text_indents = {}
if hasattr(filename_or_stream, 'read'): if hasattr(filename_or_stream, 'read'):
stream = filename_or_stream stream = filename_or_stream
@ -567,9 +569,21 @@ class MobiReader(object):
elif tag.tag == 'img': elif tag.tag == 'img':
tag.set('width', width) tag.set('width', width)
else: 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('-'): if width.startswith('-'):
styles.append('margin-left: %s' % self.ensure_unit(width[1:])) 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'): if attrib.has_key('align'):
align = attrib.pop('align').strip() align = attrib.pop('align').strip()
if align: if align:
@ -661,6 +675,26 @@ class MobiReader(object):
if hasattr(parent, 'remove'): if hasattr(parent, 'remove'):
parent.remove(tag) 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): def create_opf(self, htmlfile, guide=None, root=None):
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi) mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
if mi is None: if mi is None:
@ -731,16 +765,45 @@ class MobiReader(object):
except: except:
text = '' text = ''
text = ent_pat.sub(entity_to_unicode, 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) text)
item.left_space = int(self.get_left_whitespace(x))
found = True found = True
if reached and found and x.get('class', None) == 'mbp_pagebreak': if reached and found and x.get('class', None) == 'mbp_pagebreak':
break break
if tocobj is not None: if tocobj is not None:
tocobj = self.structure_toc(tocobj)
opf.set_toc(tocobj) opf.set_toc(tocobj)
return opf, ncx_manifest_entry 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_entries(self, data):
def sizeof_trailing_entry(ptr, psize): def sizeof_trailing_entry(ptr, psize):

View File

@ -8,23 +8,18 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, re, uuid, logging import os, re, uuid, logging
from mimetypes import types_map
from collections import defaultdict from collections import defaultdict
from itertools import count from itertools import count
from urlparse import urldefrag, urlparse, urlunparse, urljoin from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote from urllib import unquote as urlunquote
from lxml import etree, html from lxml import etree, html
from cssutils import CSSParser, parseString, parseStyle, replaceUrls from calibre.constants import filesystem_encoding, __version__
from cssutils.css import CSSRule
import calibre
from calibre.constants import filesystem_encoding
from calibre.translations.dynamic import translate from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.ebooks.conversion.preprocess import CSSPreProcessor 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) 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 If the ``link_repl_func`` returns None, the attribute or
tag text will be removed completely. tag text will be removed completely.
''' '''
from cssutils import parseString, parseStyle, replaceUrls, log
log.setLevel(logging.WARN)
if resolve_base_href: if resolve_base_href:
resolve_base_href(root) resolve_base_href(root)
for el, attrib, link, pos in iterlinks(root, find_links_in_css=False): 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 el.attrib['style'] = repl
types_map = get_types_map()
EPUB_MIME = types_map['.epub'] EPUB_MIME = types_map['.epub']
XHTML_MIME = types_map['.xhtml'] XHTML_MIME = types_map['.xhtml']
CSS_MIME = types_map['.css'] CSS_MIME = types_map['.css']
@ -1075,7 +1073,9 @@ class Manifest(object):
def _parse_css(self, data): 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): def get_style_rules_from_import(import_rule):
ans = [] ans = []
if not import_rule.styleSheet: if not import_rule.styleSheet:
@ -2011,7 +2011,7 @@ class OEBBook(object):
name='dtb:uid', content=unicode(self.uid)) name='dtb:uid', content=unicode(self.uid))
etree.SubElement(head, NCX('meta'), etree.SubElement(head, NCX('meta'),
name='dtb:depth', content=str(self.toc.depth())) name='dtb:depth', content=str(self.toc.depth()))
generator = ''.join(['calibre (', calibre.__version__, ')']) generator = ''.join(['calibre (', __version__, ')'])
etree.SubElement(head, NCX('meta'), etree.SubElement(head, NCX('meta'),
name='dtb:generator', content=generator) name='dtb:generator', content=generator)
etree.SubElement(head, NCX('meta'), etree.SubElement(head, NCX('meta'),

View File

@ -10,11 +10,9 @@ import sys, os, uuid, copy, re, cStringIO
from itertools import izip from itertools import izip
from urlparse import urldefrag, urlparse from urlparse import urldefrag, urlparse
from urllib import unquote as urlunquote from urllib import unquote as urlunquote
from mimetypes import guess_type
from collections import defaultdict from collections import defaultdict
from lxml import etree from lxml import etree
import cssutils
from calibre.ebooks.oeb.base import OPF1_NS, OPF2_NS, OPF2_NSMAP, DC11_NS, \ from calibre.ebooks.oeb.base import OPF1_NS, OPF2_NS, OPF2_NSMAP, DC11_NS, \
DC_NSES, OPF, xml2text DC_NSES, OPF, xml2text
@ -30,6 +28,7 @@ from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.utils.localization import get_lang from calibre.utils.localization import get_lang
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.constants import __appname__, __version__ from calibre.constants import __appname__, __version__
from calibre import guess_type
__all__ = ['OEBReader'] __all__ = ['OEBReader']
@ -172,6 +171,7 @@ class OEBReader(object):
return bad return bad
def _manifest_add_missing(self, invalid): def _manifest_add_missing(self, invalid):
import cssutils
manifest = self.oeb.manifest manifest = self.oeb.manifest
known = set(manifest.hrefs) known = set(manifest.hrefs)
unchecked = set(manifest.values()) unchecked = set(manifest.values())

View File

@ -12,17 +12,19 @@ import os, itertools, re, logging, copy, unicodedata
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
from xml.dom import SyntaxErr as CSSSyntaxError from xml.dom import SyntaxErr as CSSSyntaxError
import cssutils import cssutils
from cssutils.css import CSSStyleRule, CSSPageRule, CSSStyleDeclaration, \ from cssutils.css import (CSSStyleRule, CSSPageRule, CSSStyleDeclaration,
CSSValueList, CSSFontFaceRule, cssproperties CSSValueList, CSSFontFaceRule, cssproperties)
from cssutils import profile as cssprofiles from cssutils import profile as cssprofiles
from lxml import etree from lxml import etree
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
from calibre import force_unicode 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 XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
from calibre.ebooks.oeb.profile import PROFILES from calibre.ebooks.oeb.profile import PROFILES
cssutils.log.setLevel(logging.WARN)
_html_css_stylesheet = None _html_css_stylesheet = None
def html_css_stylesheet(): def html_css_stylesheet():
@ -443,7 +445,6 @@ class Stylizer(object):
class Style(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)') MS_PAT = re.compile(r'^\s*(mso-|panose-|text-underline|tab-interval)')
def __init__(self, element, stylizer): def __init__(self, element, stylizer):
@ -506,43 +507,11 @@ class Style(object):
return result return result
def _unit_convert(self, value, base=None, font=None): def _unit_convert(self, value, base=None, font=None):
' Return value in pts' 'Return value in pts'
if isinstance(value, (int, long, float)): if base is None:
return value base = self.width
try: font = font or self.fontSize
return float(value) * 72.0 / self._profile.dpi return unit_convert(value, base, font, 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
def pt_to_px(self, value): def pt_to_px(self, value):
return (self._profile.dpi / 72.0) * value return (self._profile.dpi / 72.0) * value

View File

@ -9,7 +9,6 @@ import posixpath
from urlparse import urldefrag, urlparse from urlparse import urldefrag, urlparse
from lxml import etree from lxml import etree
import cssutils
from calibre.ebooks.oeb.base import rewrite_links, urlnormalize from calibre.ebooks.oeb.base import rewrite_links, urlnormalize
@ -25,6 +24,7 @@ class RenameFiles(object): # {{{
self.renamed_items_map = renamed_items_map self.renamed_items_map = renamed_items_map
def __call__(self, oeb, opts): def __call__(self, oeb, opts):
import cssutils
self.log = oeb.logger self.log = oeb.logger
self.opts = opts self.opts = opts
self.oeb = oeb self.oeb = oeb

View File

@ -8,8 +8,6 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
from urlparse import urldefrag from urlparse import urldefrag
import cssutils
from calibre.ebooks.oeb.base import CSS_MIME, OEB_DOCS from calibre.ebooks.oeb.base import CSS_MIME, OEB_DOCS
from calibre.ebooks.oeb.base import urlnormalize, iterlinks from calibre.ebooks.oeb.base import urlnormalize, iterlinks
@ -23,6 +21,7 @@ class ManifestTrimmer(object):
return cls() return cls()
def __call__(self, oeb, context): def __call__(self, oeb, context):
import cssutils
oeb.logger.info('Trimming unused files from manifest...') oeb.logger.info('Trimming unused files from manifest...')
self.opts = context self.opts = context
used = set() used = set()

View File

@ -21,7 +21,6 @@ except ImportError:
import cStringIO import cStringIO
from calibre.ebooks.pdb.formatwriter import FormatWriter 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.pdb.header import PdbHeaderBuilder
from calibre.ebooks.pml.pmlml import PMLMLizer from calibre.ebooks.pml.pmlml import PMLMLizer
@ -135,6 +134,7 @@ class Writer(FormatWriter):
62-...: Raw image data in 8 bit PNG format. 62-...: Raw image data in 8 bit PNG format.
''' '''
images = [] images = []
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
for item in manifest: for item in manifest:
if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys():

View File

@ -18,7 +18,6 @@ from calibre.customize.conversion import OutputFormatPlugin
from calibre.customize.conversion import OptionRecommendation from calibre.customize.conversion import OptionRecommendation
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
from calibre.ebooks.pml.pmlml import PMLMLizer from calibre.ebooks.pml.pmlml import PMLMLizer
class PMLOutput(OutputFormatPlugin): class PMLOutput(OutputFormatPlugin):
@ -60,6 +59,7 @@ class PMLOutput(OutputFormatPlugin):
pmlz.add_dir(tdir) pmlz.add_dir(tdir)
def write_images(self, manifest, image_hrefs, out_dir, opts): def write_images(self, manifest, image_hrefs, out_dir, opts):
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
for item in manifest: for item in manifest:
if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys():
if opts.full_image_depth: if opts.full_image_depth:

View File

@ -12,8 +12,6 @@ import re
from lxml import etree 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.pdb.ereader import image_name
from calibre.ebooks.pml import unipmlcode from calibre.ebooks.pml import unipmlcode
@ -110,6 +108,9 @@ class PMLMLizer(object):
return output return output
def get_cover_page(self): def get_cover_page(self):
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.ebooks.oeb.base import XHTML
output = u'' output = u''
if 'cover' in self.oeb_book.guide: if 'cover' in self.oeb_book.guide:
output += '\\m="cover.png"\n' output += '\\m="cover.png"\n'
@ -125,6 +126,9 @@ class PMLMLizer(object):
return output return output
def get_text(self): def get_text(self):
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.ebooks.oeb.base import XHTML
text = [u''] text = [u'']
for item in self.oeb_book.spine: for item in self.oeb_book.spine:
self.log.debug('Converting %s to PML markup...' % item.href) self.log.debug('Converting %s to PML markup...' % item.href)
@ -214,6 +218,8 @@ class PMLMLizer(object):
return text return text
def dump_text(self, elem, stylizer, page, tag_stack=[]): 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) \ if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS: or namespace(elem.tag) != XHTML_NS:
return [] return []

View File

@ -11,8 +11,6 @@ Transform OEB content into RB compatible markup.
import re import re
from calibre import prepare_string_for_xml 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 from calibre.ebooks.rb import unique_name
TAGS = [ TAGS = [
@ -81,6 +79,8 @@ class RBMLizer(object):
return output return output
def get_cover_page(self): def get_cover_page(self):
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.ebooks.oeb.base import XHTML
output = u'' output = u''
if 'cover' in self.oeb_book.guide: if 'cover' in self.oeb_book.guide:
if self.name_map.get(self.oeb_book.guide['cover'].href, None): if self.name_map.get(self.oeb_book.guide['cover'].href, None):
@ -109,6 +109,9 @@ class RBMLizer(object):
return ''.join(toc) return ''.join(toc)
def get_text(self): def get_text(self):
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.ebooks.oeb.base import XHTML
output = [u''] output = [u'']
for item in self.oeb_book.spine: for item in self.oeb_book.spine:
self.log.debug('Converting %s to RocketBook HTML...' % item.href) self.log.debug('Converting %s to RocketBook HTML...' % item.href)
@ -137,6 +140,8 @@ class RBMLizer(object):
return text return text
def dump_text(self, elem, stylizer, page, tag_stack=[]): 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) \ if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS: or namespace(elem.tag) != XHTML_NS:
return [u''] return [u'']

View File

@ -18,7 +18,6 @@ import cStringIO
from calibre.ebooks.rb.rbml import RBMLizer from calibre.ebooks.rb.rbml import RBMLizer
from calibre.ebooks.rb import HEADER from calibre.ebooks.rb import HEADER
from calibre.ebooks.rb import unique_name from calibre.ebooks.rb import unique_name
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
from calibre.constants import __appname__, __version__ from calibre.constants import __appname__, __version__
TEXT_RECORD_SIZE = 4096 TEXT_RECORD_SIZE = 4096
@ -111,6 +110,7 @@ class RBWriter(object):
return (size, pages) return (size, pages)
def _images(self, manifest): def _images(self, manifest):
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
images = [] images = []
used_names = [] used_names = []

View File

@ -14,9 +14,6 @@ import cStringIO
from lxml import etree 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.ebooks.metadata import authors_to_string
from calibre.utils.filenames import ascii_text from calibre.utils.filenames import ascii_text
from calibre.utils.magick.draw import save_cover_data_to, identify_data from calibre.utils.magick.draw import save_cover_data_to, identify_data
@ -100,6 +97,8 @@ class RTFMLizer(object):
return self.mlize_spine() return self.mlize_spine()
def mlize_spine(self): def mlize_spine(self):
from calibre.ebooks.oeb.base import XHTML
from calibre.ebooks.oeb.stylizer import Stylizer
output = self.header() output = self.header()
if 'titlepage' in self.oeb_book.guide: if 'titlepage' in self.oeb_book.guide:
href = self.oeb_book.guide['titlepage'].href href = self.oeb_book.guide['titlepage'].href
@ -154,6 +153,8 @@ class RTFMLizer(object):
return ' }' return ' }'
def insert_images(self, text): def insert_images(self, text):
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
for item in self.oeb_book.manifest: for item in self.oeb_book.manifest:
if item.media_type in OEB_RASTER_IMAGES: if item.media_type in OEB_RASTER_IMAGES:
src = os.path.basename(item.href) src = os.path.basename(item.href)
@ -201,6 +202,8 @@ class RTFMLizer(object):
return text return text
def dump_text(self, elem, stylizer, tag_stack=[]): def dump_text(self, elem, stylizer, tag_stack=[]):
from calibre.ebooks.oeb.base import XHTML_NS, namespace, barename
if not isinstance(elem.tag, basestring) \ if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS: or namespace(elem.tag) != XHTML_NS:
p = elem.getparent() p = elem.getparent()

View File

@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en'
import os, uuid import os, uuid
from calibre.customize.conversion import InputFormatPlugin from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.oeb.base import DirContainer
from calibre.ebooks.snb.snbfile import SNBFile from calibre.ebooks.snb.snbfile import SNBFile
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
@ -30,6 +29,7 @@ class SNBInput(InputFormatPlugin):
def convert(self, stream, options, file_ext, log, def convert(self, stream, options, file_ext, log,
accelerators): accelerators):
from calibre.ebooks.oeb.base import DirContainer
log.debug("Parsing SNB file...") log.debug("Parsing SNB file...")
snbFile = SNBFile() snbFile = SNBFile()
try: try:

View File

@ -5,7 +5,8 @@ __copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, struct, zlib, bz2, os import sys, struct, zlib, bz2, os
from mimetypes import types_map
from calibre import guess_type
class FileStream: class FileStream:
def IsBinary(self): def IsBinary(self):
@ -180,7 +181,7 @@ class SNBFile:
file = open(os.path.join(path, fname), 'wb') file = open(os.path.join(path, fname), 'wb')
file.write(f.fileBody) file.write(f.fileBody)
file.close() file.close()
fileNames.append((fname, types_map[ext])) fileNames.append((fname, guess_type('a'+ext)[0]))
return fileNames return fileNames
def Output(self, outputFile): def Output(self, outputFile):

View File

@ -13,8 +13,6 @@ import re
from lxml import etree 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): def ProcessFileName(fileName):
# Flat the path # Flat the path
@ -81,6 +79,8 @@ class SNBMLizer(object):
body.append(entity) body.append(entity)
def mlize(self): def mlize(self):
from calibre.ebooks.oeb.base import XHTML
from calibre.ebooks.oeb.stylizer import Stylizer
output = [ u'' ] output = [ u'' ]
stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile) 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)) content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode))
@ -208,6 +208,7 @@ class SNBMLizer(object):
return text return text
def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''): 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) \ if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS: or namespace(elem.tag) != XHTML_NS:

View File

@ -11,7 +11,6 @@ from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ebooks.oeb.base import OEB_IMAGES
from calibre.ebooks.txt.txtml import TXTMLizer from calibre.ebooks.txt.txtml import TXTMLizer
from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines
from calibre.ptempfile import TemporaryDirectory, TemporaryFile from calibre.ptempfile import TemporaryDirectory, TemporaryFile
@ -109,6 +108,7 @@ class TXTZOutput(TXTOutput):
file_type = 'txtz' file_type = 'txtz'
def convert(self, oeb_book, output_path, input_plugin, opts, log): 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: with TemporaryDirectory('_txtz_output') as tdir:
# TXT # TXT
with TemporaryFile('index.txt') as tf: with TemporaryFile('index.txt') as tf:

View File

@ -12,8 +12,6 @@ import re
from lxml import etree from lxml import etree
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace
from calibre.ebooks.oeb.stylizer import Stylizer
BLOCK_TAGS = [ BLOCK_TAGS = [
'div', 'div',
@ -64,6 +62,8 @@ class TXTMLizer(object):
return self.mlize_spine() return self.mlize_spine()
def mlize_spine(self): def mlize_spine(self):
from calibre.ebooks.oeb.base import XHTML
from calibre.ebooks.oeb.stylizer import Stylizer
output = [u''] output = [u'']
output.append(self.get_toc()) output.append(self.get_toc())
for item in self.oeb_book.spine: for item in self.oeb_book.spine:
@ -185,6 +185,7 @@ class TXTMLizer(object):
@stylizer: The style information attached to the element. @stylizer: The style information attached to the element.
@page: OEB page used to determine absolute urls. @page: OEB page used to determine absolute urls.
''' '''
from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace
if not isinstance(elem.tag, basestring) \ if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS: or namespace(elem.tag) != XHTML_NS:

View File

@ -4,19 +4,17 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, Queue, threading import os, sys, Queue, threading
from threading import RLock from threading import RLock
from urllib import unquote from urllib import unquote
from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ QByteArray, QTranslator, QCoreApplication, QThread,
QByteArray, QTranslator, QCoreApplication, QThread, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices,
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QFileDialog, QFileIconProvider,
QFileDialog, QFileIconProvider, \ QIcon, QApplication, QDialog, QUrl, QFont)
QIcon, QApplication, QDialog, QUrl, QFont
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator 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.ebooks.metadata import MetaInformation
from calibre.utils.date import UNDEFINED_DATE from calibre.utils.date import UNDEFINED_DATE
@ -156,7 +154,9 @@ def _config():
c.add_opt('plugin_search_history', default=[], c.add_opt('plugin_search_history', default=[],
help='Search history for the recipe scheduler') help='Search history for the recipe scheduler')
c.add_opt('worker_limit', default=6, 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, c.add_opt('get_social_metadata', default=True,
help=_('Download social metadata (tags/rating/etc.)')) help=_('Download social metadata (tags/rating/etc.)'))
c.add_opt('overwrite_author_title_metadata', default=True, c.add_opt('overwrite_author_title_metadata', default=True,
@ -330,6 +330,7 @@ class GetMetadata(QObject):
id, args, kwargs) id, args, kwargs)
def _from_formats(self, id, args, kwargs): def _from_formats(self, id, args, kwargs):
from calibre.ebooks.metadata.meta import metadata_from_formats
try: try:
mi = metadata_from_formats(*args, **kwargs) mi = metadata_from_formats(*args, **kwargs)
except: except:
@ -337,6 +338,7 @@ class GetMetadata(QObject):
self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi) self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
def _get_metadata(self, id, args, kwargs): def _get_metadata(self, id, args, kwargs):
from calibre.ebooks.metadata.meta import get_metadata
try: try:
mi = get_metadata(*args, **kwargs) mi = get_metadata(*args, **kwargs)
except: except:
@ -738,3 +740,4 @@ def build_forms(srcdir, info=None):
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None) _df = os.environ.get('CALIBRE_DEVELOP_FROM', None)
if _df and os.path.exists(_df): if _df and os.path.exists(_df):
build_forms(_df) build_forms(_df)

View File

@ -22,7 +22,7 @@ class FetchAnnotationsAction(InterfaceAction):
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):
pass self.qaction.triggered.connect(self.fetch_annotations)
def fetch_annotations(self, *args): def fetch_annotations(self, *args):
# Generate a path_map from selected ids # Generate a path_map from selected ids
@ -52,6 +52,10 @@ class FetchAnnotationsAction(InterfaceAction):
return path_map return path_map
device = self.gui.device_manager.device 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: if self.gui.current_view() is not self.gui.library_view:
return error_dialog(self.gui, _('Use library only'), return error_dialog(self.gui, _('Use library only'),

View File

@ -37,8 +37,6 @@ class EditMetadataAction(InterfaceAction):
md.addSeparator() md.addSeparator()
if test_eight_code: if test_eight_code:
dall = self.download_metadata dall = self.download_metadata
dident = partial(self.download_metadata, covers=False)
dcovers = partial(self.download_metadata, identify=False)
else: else:
dall = partial(self.download_metadata_old, False, covers=True) dall = partial(self.download_metadata_old, False, covers=True)
dident = partial(self.download_metadata_old, False, covers=False) dident = partial(self.download_metadata_old, False, covers=False)
@ -47,9 +45,9 @@ class EditMetadataAction(InterfaceAction):
md.addAction(_('Download metadata and covers'), dall, md.addAction(_('Download metadata and covers'), dall,
Qt.ControlModifier+Qt.Key_D) Qt.ControlModifier+Qt.Key_D)
md.addAction(_('Download only metadata'), dident)
md.addAction(_('Download only covers'), dcovers)
if not test_eight_code: if not test_eight_code:
md.addAction(_('Download only metadata'), dident)
md.addAction(_('Download only covers'), dcovers)
md.addAction(_('Download only social metadata'), md.addAction(_('Download only social metadata'),
partial(self.download_metadata_old, False, covers=False, partial(self.download_metadata_old, False, covers=False,
set_metadata=False, set_social_metadata=True)) set_metadata=False, set_social_metadata=True))
@ -80,7 +78,7 @@ class EditMetadataAction(InterfaceAction):
self.qaction.setEnabled(enabled) self.qaction.setEnabled(enabled)
self.action_merge.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: if ids is None:
rows = self.gui.library_view.selectionModel().selectedRows() rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
@ -90,7 +88,7 @@ class EditMetadataAction(InterfaceAction):
ids = [db.id(row.row()) for row in rows] ids = [db.id(row.row()) for row in rows]
from calibre.gui2.metadata.bulk_download2 import start_download from calibre.gui2.metadata.bulk_download2 import start_download
start_download(self.gui, ids, start_download(self.gui, ids,
Dispatcher(self.bulk_metadata_downloaded), identify, covers) Dispatcher(self.bulk_metadata_downloaded))
def bulk_metadata_downloaded(self, job): def bulk_metadata_downloaded(self, job):
if job.failed: if job.failed:

View File

@ -47,16 +47,21 @@ class StoreAction(InterfaceAction):
if self.config.get('first_run', True): if self.config.get('first_run', True):
self.config['first_run'] = False self.config['first_run'] = False
from calibre.gui2 import info_dialog from calibre.gui2 import info_dialog
info_dialog(self.gui, _('Get Books Disclaimer'), info_dialog(self.gui, _('About Get Books'), '<p>' +
_('<p>Calibre helps you find books to read by connecting you with outside stores. ' _('Calibre helps you find the ebooks you want by searching '
'The stores are a variety of big, independent, free, and public domain sources.</p>' 'the websites of a variety of commercial and public domain '
'<p>Using the integrated search you can easily find what store has the book you\'re ' 'book sources for you.') +
'looking for. It will also give you a price, DRM status as well as a lot of ' '<p>' +
'other useful information.</p>' _('Using the integrated search you can easily find which '
'<p>All transaction (paid or otherwise) are handled between you and the store. ' 'store has the book you are looking for, at the best price. '
'Calibre is not part of this process and any issues related to a purchase need to ' 'You will also get DRM status and other useful information.')
'be directed to the actual store. Be sure to double check that any books you get ' + '<p>' +
'will work with you device. Double check for format and ' _('All transactions (paid or otherwise) are handled between '
'<a href="http://en.wikipedia.org/wiki/Digital_rights_management">DRM</a> ' 'you and the particular website. '
'restrictions.</p>'), 'Calibre is not part of this process and any issues related '
show=True, show_copy_button=False) '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 '
'<a href="http://drmfree.calibre-ebook.com/about#drm">DRM</a>.'
), show=True, show_copy_button=False)

View File

@ -418,6 +418,7 @@ class BookDetails(QWidget): # {{{
if y is None: if y is None:
# Local image # Local image
self.cover_view.paste_from_clipboard(x) self.cover_view.paste_from_clipboard(x)
self.update_layout()
else: else:
self.remote_file_dropped.emit(x, y) self.remote_file_dropped.emit(x, y)
# We do not support setting cover *and* adding formats for # We do not support setting cover *and* adding formats for
@ -449,6 +450,7 @@ class BookDetails(QWidget): # {{{
self.setAcceptDrops(True) self.setAcceptDrops(True)
self._layout = DetailsLayout(vertical, self) self._layout = DetailsLayout(vertical, self)
self.setLayout(self._layout) self.setLayout(self._layout)
self.current_path = ''
self.cover_view = CoverView(vertical, self) self.cover_view = CoverView(vertical, self)
self.cover_view.cover_changed.connect(self.cover_changed.emit) self.cover_view.cover_changed.connect(self.cover_changed.emit)
@ -482,15 +484,19 @@ class BookDetails(QWidget): # {{{
def show_data(self, data): def show_data(self, data):
self.book_info.show_data(data) self.book_info.show_data(data)
self.cover_view.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()) self._layout.do_layout(self.rect())
try: try:
sz = self.cover_view.pixmap.size() sz = self.cover_view.pixmap.size()
except: except:
sz = QSize(0, 0) sz = QSize(0, 0)
self.setToolTip( self.setToolTip(
'<p>'+_('Double-click to open Book Details window') + '<p>'+_('Double-click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), '') + '<br><br>' + _('Path') + ': ' + self.current_path +
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height()) '<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
) )
def reset_info(self): def reset_info(self):

View File

@ -289,6 +289,7 @@ class Series(Base):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
w = MultiCompleteComboBox(parent) w = MultiCompleteComboBox(parent)
w.set_separator(None)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25) w.setMinimumContentsLength(25)
self.name_widget = w self.name_widget = w

View File

@ -7,7 +7,7 @@ import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread from threading import Thread
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \ 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, \ from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins device_plugins
@ -25,12 +25,10 @@ from calibre.devices.errors import FreeSpaceError
from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.apple.driver import ITUNES_ASYNC
from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.folder_device.driver import FOLDER_DEVICE
from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.magick.draw import thumbnail from calibre.utils.magick.draw import thumbnail
from calibre.library.save_to_disk import plugboard_any_device_value, \ from calibre.library.save_to_disk import find_plugboard
plugboard_any_format_value
# }}} # }}}
class DeviceJob(BaseJob): # {{{ 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): def device_name_for_plugboards(device_class):
if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'): if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'):
return 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): def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None):
'''Upload books to device: ''' '''Upload books to device: '''
from calibre.ebooks.metadata.meta import set_metadata
if hasattr(self.connected_device, 'set_plugboards') and \ if hasattr(self.connected_device, 'set_plugboards') and \
callable(self.connected_device.set_plugboards): callable(self.connected_device.set_plugboards):
self.connected_device.set_plugboards(plugboards, find_plugboard) 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): # {{{ class DeviceMixin(object): # {{{
def __init__(self): def __init__(self):
@ -753,6 +753,7 @@ class DeviceMixin(object): # {{{
self.location_manager.update_devices() self.location_manager.update_devices()
self.library_view.set_device_connected(self.device_connected) self.library_view.set_device_connected(self.device_connected)
self.refresh_ondevice() self.refresh_ondevice()
device_signals.device_connection_changed.emit(connected)
def info_read(self, job): def info_read(self, job):
''' '''
@ -791,6 +792,7 @@ class DeviceMixin(object): # {{{
self.sync_news() self.sync_news()
self.sync_catalogs() self.sync_catalogs()
self.refresh_ondevice() self.refresh_ondevice()
device_signals.device_metadata_available.emit()
def refresh_ondevice(self, reset_only = False): def refresh_ondevice(self, reset_only = False):
''' '''

View File

@ -24,11 +24,16 @@ class Dialog(QDialog, Ui_Dialog):
dynamic[confirm_config_name(self.name)] = self.again.isChecked() 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): if not dynamic.get(confirm_config_name(name), True):
return True return True
d = Dialog(msg, name, parent) d = Dialog(msg, name, parent)
d.label.setPixmap(QPixmap(I(pixmap))) d.label.setPixmap(QPixmap(I(pixmap)))
d.setWindowIcon(QIcon(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()) d.resize(d.sizeHint())
return d.exec_() == d.Accepted return d.exec_() == d.Accepted

View File

@ -13,7 +13,6 @@ from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor 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 import string_to_authors, authors_to_string, title_sort
from calibre.ebooks.metadata.book.base import composite_formatter 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.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, \ from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, \
gprefs, question_dialog gprefs, question_dialog
@ -26,6 +25,7 @@ from calibre.utils.magick.draw import identify_data
from calibre.utils.date import qt_to_dt from calibre.utils.date import qt_to_dt
def get_cover_data(path): # {{{ def get_cover_data(path): # {{{
from calibre.ebooks.metadata.meta import get_metadata
old = prefs['read_file_metadata'] old = prefs['read_file_metadata']
if not old: if not old:
prefs['read_file_metadata'] = True prefs['read_file_metadata'] = True

View File

@ -25,7 +25,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import string_to_authors, \ from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn, title_sort authors_to_string, check_isbn, title_sort
from calibre.ebooks.metadata.covers import download_cover from calibre.ebooks.metadata.covers import download_cover
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
@ -353,6 +352,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.formats_changed = True self.formats_changed = True
def get_selected_format_metadata(self): def get_selected_format_metadata(self):
from calibre.ebooks.metadata.meta import get_metadata
old = prefs['read_file_metadata'] old = prefs['read_file_metadata']
if not old: if not old:
prefs['read_file_metadata'] = True prefs['read_file_metadata'] = True

View File

@ -12,7 +12,7 @@ from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog 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 import open_local_file
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract from calibre.libunzip import extract as zipextract

View File

@ -156,8 +156,6 @@ class SearchBar(QWidget): # {{{
x = ComboBoxWithHelp(self) x = ComboBoxWithHelp(self)
x.setMaximumSize(QSize(150, 16777215)) x.setMaximumSize(QSize(150, 16777215))
x.setObjectName("search_restriction") x.setObjectName("search_restriction")
x.setToolTip(_('Books display will be restricted to those matching the '
'selected saved search'))
l.addWidget(x) l.addWidget(x)
parent.search_restriction = x parent.search_restriction = x

View File

@ -18,7 +18,6 @@ from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.utils.icu import sort_key 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.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH, MetadataBackup, force_to_bool REGEXP_MATCH, MetadataBackup, force_to_bool
@ -478,6 +477,7 @@ class BooksModel(QAbstractTableModel): # {{{
def get_preferred_formats_from_ids(self, ids, formats, def get_preferred_formats_from_ids(self, ids, formats,
set_metadata=False, specific_format=None, set_metadata=False, specific_format=None,
exclude_auto=False, mode='r+b'): exclude_auto=False, mode='r+b'):
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
ans = [] ans = []
need_auto = [] need_auto = []
if specific_format is not None: if specific_format is not None:
@ -526,6 +526,7 @@ class BooksModel(QAbstractTableModel): # {{{
def get_preferred_formats(self, rows, formats, paths=False, def get_preferred_formats(self, rows, formats, paths=False,
set_metadata=False, specific_format=None, set_metadata=False, specific_format=None,
exclude_auto=False): exclude_auto=False):
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
ans = [] ans = []
need_auto = [] need_auto = []
if specific_format is not None: if specific_format is not None:

View File

@ -19,6 +19,9 @@ from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.library.sqlite import sqlite, DatabaseException from calibre.library.sqlite import sqlite, DatabaseException
if iswindows:
winutil = plugins['winutil'][0]
def option_parser(): def option_parser():
parser = _option_parser('''\ parser = _option_parser('''\
%prog [opts] [path_to_ebook] %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 if library_path is None: # Need to migrate to new database layout
base = os.path.expanduser('~') base = os.path.expanduser('~')
if iswindows: if iswindows:
base = plugins['winutil'][0].special_folder_path( base = winutil.special_folder_path(winutil.CSIDL_PERSONAL)
plugins['winutil'][0].CSIDL_PERSONAL)
if not base or not os.path.exists(base): if not base or not os.path.exists(base):
from PyQt4.Qt import QDir from PyQt4.Qt import QDir
base = unicode(QDir.homePath()).replace('/', os.sep) base = unicode(QDir.homePath()).replace('/', os.sep)

View File

@ -24,7 +24,7 @@ from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog choose_files, error_dialog, choose_images, question_dialog
from calibre.utils.date import local_tz, qt_to_dt 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.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre.utils.date import utcfromtimestamp from calibre.utils.date import utcfromtimestamp
@ -278,11 +278,13 @@ class AuthorSortEdit(EnLineEdit):
def copy_to_authors(self): def copy_to_authors(self):
aus = self.current_val aus = self.current_val
meth = tweaks['author_sort_copy_method']
if aus: if aus:
ln, _, rest = aus.partition(',') ln, _, rest = aus.partition(',')
if rest: if rest:
au = rest.strip() + ' ' + ln.strip() if meth in ('invert', 'nocomma', 'comma'):
self.authors_edit.current_val = [au] aus = rest.strip() + ' ' + ln.strip()
self.authors_edit.current_val = [aus]
def auto_generate(self, *args): def auto_generate(self, *args):
au = unicode(self.authors_edit.text()) 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 = QToolButton(self)
self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) 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.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 = QToolButton(self)
self.add_format_button.setIcon(QIcon(I('add_book.png'))) self.add_format_button.setIcon(QIcon(I('add_book.png')))
self.add_format_button.setIconSize(QSize(32, 32)) self.add_format_button.setIconSize(QSize(32, 32))
self.add_format_button.clicked.connect(self.add_format) 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 = QToolButton(self)
self.remove_format_button.setIcon(QIcon(I('trash.png'))) self.remove_format_button.setIcon(QIcon(I('trash.png')))
self.remove_format_button.setIconSize(QSize(32, 32)) self.remove_format_button.setIconSize(QSize(32, 32))
self.remove_format_button.clicked.connect(self.remove_format) 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 = FormatList(self)
self.formats.setAcceptDrops(True) self.formats.setAcceptDrops(True)
@ -664,12 +672,7 @@ class Cover(ImageView): # {{{
self.frame_size = (sz.width()//3, sz.height()) self.frame_size = (sz.width()//3, sz.height())
def sizeHint(self): def sizeHint(self):
sz = ImageView.sizeHint(self) sz = QSize(self.frame_size[0], self.frame_size[1])
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)
return sz return sz
def select_cover(self, *args): def select_cover(self, *args):
@ -939,7 +942,13 @@ class IdentifiersEdit(QLineEdit): # {{{
def fset(self, val): def fset(self, val):
if not val: if not val:
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.setText(txt.strip())
self.setCursorPosition(0) self.setCursorPosition(0)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@ -959,7 +968,7 @@ class IdentifiersEdit(QLineEdit): # {{{
tt = self.BASE_TT tt = self.BASE_TT
extra = '' extra = ''
if not isbn: if not isbn:
col = 'rgba(0,255,0,0%)' col = 'none'
elif check_isbn(isbn) is not None: elif check_isbn(isbn) is not None:
col = 'rgba(0,255,0,20%)' col = 'rgba(0,255,0,20%)'
extra = '\n\n'+_('This ISBN number is valid') extra = '\n\n'+_('This ISBN number is valid')

View File

@ -12,7 +12,8 @@ from functools import partial
from itertools import izip from itertools import izip
from PyQt4.Qt import (QIcon, QDialog, QVBoxLayout, QTextBrowser, QSize, 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.dialogs.message_box import MessageBox
from calibre.gui2.threaded_jobs import ThreadedJob 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.customize.ui import metadata_plugins
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
# Start download {{{
def show_config(gui, parent): def show_config(gui, parent):
from calibre.gui2.preferences import show_config_widget from calibre.gui2.preferences import show_config_widget
show_config_widget('Sharing', 'Metadata download', parent=parent, show_config_widget('Sharing', 'Metadata download', parent=parent,
gui=gui, never_shutdown=True) gui=gui, never_shutdown=True)
def start_download(gui, ids, callback, identify, covers): class ConfirmDialog(QDialog):
q = MessageBox(MessageBox.QUESTION, _('Schedule download?'),
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(
'<p>'+_('The download of metadata for the <b>%d selected book(s)</b> will' '<p>'+_('The download of metadata for the <b>%d selected book(s)</b> will'
' run in the background. Proceed?')%len(ids) + ' run in the background. Proceed?')%len(ids) +
'<p>'+_('You can monitor the progress of the download ' '<p>'+_('You can monitor the progress of the download '
'by clicking the rotating spinner in the bottom right ' 'by clicking the rotating spinner in the bottom right '
'corner.') + 'corner.') +
'<p>'+_('When the download completes you will be asked for' '<p>'+_('When the download completes you will be asked for'
' confirmation before calibre applies the downloaded metadata.'), ' confirmation before calibre applies the downloaded metadata.')
show_copy_button=False, parent=gui) )
b = q.bb.addButton(_('Configure download'), q.bb.ActionRole) t.setWordWrap(True)
b.setIcon(QIcon(I('config.png'))) l.addWidget(t, 0, 1)
b.clicked.connect(partial(show_config, gui, q)) l.setColumnStretch(0, 1)
q.det_msg_toggle.setVisible(False) l.setColumnStretch(1, 100)
ret = q.exec_() self.identify = self.covers = True
b.clicked.disconnect() self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
if ret != q.Accepted: 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 return
job = ThreadedJob('metadata bulk download', job = ThreadedJob('metadata bulk download',
_('Download metadata for %d books')%len(ids), _('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.job_manager.run_threaded_job(job)
gui.status_bar.show_message(_('Metadata download started'), 3000) gui.status_bar.show_message(_('Metadata download started'), 3000)
# }}}
class ViewLog(QDialog): # {{{ class ViewLog(QDialog): # {{{
@ -93,9 +143,10 @@ def view_log(job, parent):
# }}} # }}}
# Apply downloaded metadata {{{
class ApplyDialog(QDialog): class ApplyDialog(QDialog):
def __init__(self, id_map, gui): def __init__(self, gui):
QDialog.__init__(self, gui) QDialog.__init__(self, gui)
self.l = l = QVBoxLayout() self.l = l = QVBoxLayout()
@ -104,27 +155,33 @@ class ApplyDialog(QDialog):
self.pb = QProgressBar(self) self.pb = QProgressBar(self)
l.addWidget(self.pb) l.addWidget(self.pb)
self.pb.setMinimum(0)
self.pb.setMaximum(len(id_map))
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.bb.rejected.connect(self.reject) self.bb.rejected.connect(self.reject)
self.bb.accepted.connect(self.accept)
l.addWidget(self.bb) l.addWidget(self.bb)
self.gui = gui 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.id_map = list(id_map.iteritems())
self.current_idx = 0 self.current_idx = 0
self.failures = [] self.failures = []
self.ids = [] self.ids = []
self.canceled = False self.canceled = False
self.pb.setMinimum(0)
QTimer.singleShot(20, self.do_one) self.pb.setMaximum(len(id_map))
self.timer.start(50)
def do_one(self): def do_one(self):
if self.canceled: if self.canceled:
return return
if self.current_idx >= len(self.id_map):
self.timer.stop()
self.finalize()
return
i, mi = self.id_map[self.current_idx] i, mi = self.id_map[self.current_idx]
db = self.gui.current_db db = self.gui.current_db
try: try:
@ -144,15 +201,11 @@ class ApplyDialog(QDialog):
pass pass
self.pb.setValue(self.pb.value()+1) self.pb.setValue(self.pb.value()+1)
self.current_idx += 1
if self.current_idx >= len(self.id_map) - 1:
self.finalize()
else:
self.current_idx += 1
QTimer.singleShot(20, self.do_one)
def reject(self): def reject(self):
self.canceled = True self.canceled = True
self.timer.stop()
QDialog.reject(self) QDialog.reject(self)
def finalize(self): def finalize(self):
@ -169,17 +222,18 @@ class ApplyDialog(QDialog):
title += ' - ' + authors_to_string(authors) title += ' - ' + authors_to_string(authors)
msg.append(title+'\n\n'+tb+'\n'+('*'*80)) 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' _('Failed to apply updated metadata for some books'
' in your library. Click "Show Details" to see ' ' in your library. Click "Show Details" to see '
'details.'), det_msg='\n\n'.join(msg), show=True) 'details.'), det_msg='\n\n'.join(msg), show=True)
self.accept()
if self.ids: if self.ids:
cr = self.gui.library_view.currentIndex().row() cr = self.gui.library_view.currentIndex().row()
self.gui.library_view.model().refresh_ids( self.gui.library_view.model().refresh_ids(
self.ids, cr) self.ids, cr)
if self.gui.cover_flow: if self.gui.cover_flow:
self.gui.cover_flow.dataChanged() self.gui.cover_flow.dataChanged()
self.accept()
_amd = None _amd = None
def apply_metadata(job, gui, q, result): def apply_metadata(job, gui, q, result):
@ -188,7 +242,7 @@ def apply_metadata(job, gui, q, result):
q.finished.disconnect() q.finished.disconnect()
if result != q.Accepted: if result != q.Accepted:
return 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 id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in
failed_ids]) failed_ids])
if not id_map: 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)): 'Do you want to proceed?'), det_msg='\n'.join(modified)):
return return
_amd = ApplyDialog(id_map, gui) if _amd is None:
_amd.exec_() _amd = ApplyDialog(gui)
_amd.start(id_map)
if len(id_map) > 3:
_amd.exec_()
def proceed(gui, job): def proceed(gui, job):
gui.status_bar.show_message(_('Metadata download completed'), 3000) gui.status_bar.show_message(_('Metadata download completed'), 3000)
id_map, failed_ids, failed_covers, title_map = job.result id_map, failed_ids, failed_covers, title_map, all_failed = job.result
fmsg = det_msg = '' det_msg = []
if failed_ids or failed_covers: for i in failed_ids | failed_covers:
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click' title = title_map[i]
' "Show details" to see which books.')%len(failed_ids) if i in failed_ids:
det_msg = [] title += (' ' + _('(Failed metadata)'))
for i in failed_ids | failed_covers: if i in failed_covers:
title = title_map[i] title += (' ' + _('(Failed cover)'))
if i in failed_ids: det_msg.append(title)
title += (' ' + _('(Failed metadata)')) det_msg = '\n'.join(det_msg)
if i in failed_covers:
title += (' ' + _('(Failed cover)')) if all_failed:
det_msg.append(title) q = error_dialog(gui, _('Download failed'),
msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. ' _('Failed to download metadata or covers for any of the %d'
'Proceed with updating the metadata in your library?')%len(id_map) ' book(s).') % len(id_map), det_msg=det_msg)
q = MessageBox(MessageBox.QUESTION, _('Download complete'), else:
msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), fmsg = ''
parent=gui) if failed_ids or failed_covers:
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%len(failed_ids)
msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. '
'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 = q.bb.addButton(_('View log'), q.bb.ActionRole)
q.vlb.setIcon(QIcon(I('debug.png'))) q.vlb.setIcon(QIcon(I('debug.png')))
q.vlb.clicked.connect(partial(view_log, job, q)) q.vlb.clicked.connect(partial(view_log, job, q))
q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers)) q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers))
q.setModal(False) q.setModal(False)
q.show() q.show()
q.finished.connect(partial(apply_metadata, job, gui, q))
# }}}
def merge_result(oldmi, newmi): def merge_result(oldmi, newmi):
dummy = Metadata(_('Unknown')) dummy = Metadata(_('Unknown'))
for f in msprefs['ignore_fields']: for f in msprefs['ignore_fields']:
setattr(newmi, f, getattr(dummy, f)) if ':' not in f:
setattr(newmi, f, getattr(dummy, f))
fields = set() fields = set()
for plugin in metadata_plugins(['identify']): for plugin in metadata_plugins(['identify']):
fields |= plugin.touched_fields fields |= plugin.touched_fields
@ -276,6 +344,7 @@ def download(ids, db, do_identify, covers,
title_map = {} title_map = {}
ans = {} ans = {}
count = 0 count = 0
all_failed = True
for i, mi in izip(ids, metadata): for i, mi in izip(ids, metadata):
if abort.is_set(): if abort.is_set():
log.error('Aborting...') log.error('Aborting...')
@ -290,6 +359,7 @@ def download(ids, db, do_identify, covers,
except: except:
pass pass
if results: if results:
all_failed = False
mi = merge_result(mi, results[0]) mi = merge_result(mi, results[0])
identifiers = mi.identifiers identifiers = mi.identifiers
if not mi.is_null('rating'): if not mi.is_null('rating'):
@ -307,6 +377,7 @@ def download(ids, db, do_identify, covers,
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f: with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
f.write(cdata[-1]) f.write(cdata[-1])
mi.cover = f.name mi.cover = f.name
all_failed = False
else: else:
failed_covers.add(i) failed_covers.add(i)
ans[i] = mi ans[i] = mi
@ -314,7 +385,7 @@ def download(ids, db, do_identify, covers,
notifications.put((count/len(ids), notifications.put((count/len(ids),
_('Downloaded %d of %d')%(count, len(ids)))) _('Downloaded %d of %d')%(count, len(ids))))
log('Download complete, with %d failures'%len(failed_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)

View File

@ -156,6 +156,9 @@ class MetadataSingleDialogBase(ResizableDialog):
self.identifiers = IdentifiersEdit(self) self.identifiers = IdentifiersEdit(self)
self.basic_metadata_widgets.append(self.identifiers) 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.publisher = PublisherEdit(self)
self.basic_metadata_widgets.append(self.publisher) self.basic_metadata_widgets.append(self.publisher)
@ -323,7 +326,8 @@ class MetadataSingleDialogBase(ResizableDialog):
mi = d.book mi = d.book
dummy = Metadata(_('Unknown')) dummy = Metadata(_('Unknown'))
for f in msprefs['ignore_fields']: 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: if mi is not None:
self.update_from_mi(mi) self.update_from_mi(mi)
if d.cover_pixmap is not None: if d.cover_pixmap is not None:
@ -541,8 +545,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
sto(self.rating, self.tags) sto(self.rating, self.tags)
create_row2(2, self.tags, self.tags_editor_button) create_row2(2, self.tags, self.tags_editor_button)
sto(self.tags_editor_button, self.identifiers) sto(self.tags_editor_button, self.identifiers)
create_row2(3, self.identifiers) create_row2(3, self.identifiers, self.clear_identifiers_button)
sto(self.identifiers, self.timestamp) sto(self.clear_identifiers_button, self.timestamp)
create_row2(4, self.timestamp, self.timestamp.clear_button) create_row2(4, self.timestamp, self.timestamp.clear_button)
sto(self.timestamp.clear_button, self.pubdate) sto(self.timestamp.clear_button, self.pubdate)
create_row2(5, self.pubdate, self.pubdate.clear_button) 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(9, self.publisher, self.timestamp)
create_row(10, self.timestamp, self.identifiers, create_row(10, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png') 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), tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
12, 1, 1 ,1) 12, 1, 1 ,1)

View File

@ -116,6 +116,10 @@ class CoverDelegate(QStyledItemDelegate): # {{{
def paint(self, painter, option, index): def paint(self, painter, option, index):
QStyledItemDelegate.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(): if self.timer.isActive() and index.data(Qt.UserRole).toBool():
rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect = QRect(0, 0, self.spinner_width, self.spinner_width)
rect.moveCenter(option.rect.center()) rect.moveCenter(option.rect.center())

View File

@ -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.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults)
bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).setEnabled(False)
bb.button(bb.Apply).clicked.connect(d.accept) 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() l = QVBoxLayout()
d.setLayout(l) d.setLayout(l)
l.addWidget(w) l.addWidget(w)

View File

@ -6,19 +6,27 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __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.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
from calibre.constants import isosx 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): class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui): def genesis(self, gui):
self.gui = gui self.gui = gui
r = self.register 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) r('enforce_cpu_limit', config, restart_required=True)
self.device_detection_button.clicked.connect(self.debug_device_detection) self.device_detection_button.clicked.connect(self.debug_device_detection)
self.button_open_config_dir.clicked.connect(self.open_config_dir) self.button_open_config_dir.clicked.connect(self.open_config_dir)

View File

@ -17,7 +17,7 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>&amp;Maximum number of waiting worker processes (needs restart):</string> <string>Max. simultaneous conversion/news download jobs:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>opt_worker_limit</cstring> <cstring>opt_worker_limit</cstring>
@ -27,13 +27,7 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QSpinBox" name="opt_worker_limit"> <widget class="QSpinBox" name="opt_worker_limit">
<property name="minimum"> <property name="minimum">
<number>2</number> <number>1</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>2</number>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt, QLineEdit, QComboBox, SIGNAL, QListWidgetItem 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 import error_dialog
from calibre.gui2.device import device_name_for_plugboards from calibre.gui2.device import device_name_for_plugboards
from calibre.gui2.dialogs.template_dialog import TemplateDialog 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.customize.ui import metadata_writers, device_plugins
from calibre.library.save_to_disk import plugboard_any_format_value, \ from calibre.library.save_to_disk import plugboard_any_format_value, \
plugboard_any_device_value, plugboard_save_to_disk_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 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.device_label.setText(_('Device currently connected: None'))
self.devices = ['', 'APPLE', 'FOLDER_DEVICE'] self.devices = ['', 'APPLE', 'FOLDER_DEVICE']
self.device_to_formats_map = {}
for device in device_plugins(): for device in device_plugins():
n = device_name_for_plugboards(device) n = device_name_for_plugboards(device)
self.device_to_formats_map[n] = device.FORMATS
if n not in self.devices: if n not in self.devices:
self.devices.append(n) self.devices.append(n)
self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
self.devices.insert(1, plugboard_save_to_disk_value) 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.new_device.addItems(self.devices)
self.formats = [''] self.formats = ['']
for w in metadata_writers(): for w in metadata_writers():
for f in w.file_types: if not is_disabled(w):
self.formats.append(f) for f in w.file_types:
if not f in self.formats:
self.formats.append(f)
self.formats.append('device_db') self.formats.append('device_db')
self.formats.sort() self.formats.sort()
self.formats.insert(1, plugboard_any_format_value) self.formats.insert(1, plugboard_any_format_value)
@ -230,6 +240,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
show=True) show=True)
self.new_device.setCurrentIndex(0) self.new_device.setCurrentIndex(0)
return 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() self.set_fields()
def new_format_changed(self, txt): def new_format_changed(self, txt):

View File

@ -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 import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugins_ui import Ui_Form from calibre.gui2.preferences.plugins_ui import Ui_Form
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
disable_plugin, plugin_customization, add_plugin, \ disable_plugin, plugin_customization, add_plugin,
remove_plugin remove_plugin, NameConflict)
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \ from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
question_dialog, gprefs question_dialog, gprefs
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
@ -279,7 +279,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
' Are you sure you want to proceed?'), ' Are you sure you want to proceed?'),
show_copy_button=False): show_copy_button=False):
return 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.populate()
self._plugin_model.reset() self._plugin_model.reset()
self.changed_signal.emit() self.changed_signal.emit()

View File

@ -17,6 +17,10 @@ class SearchRestrictionMixin(object):
self.search_restriction.setMinimumContentsLength(10) self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip()) self.search_restriction.setStatusTip(self.search_restriction.toolTip())
self.search_count.setText(_("(all books)")) 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): def apply_named_search_restriction(self, name):
if not name: if not name:
@ -30,29 +34,38 @@ class SearchRestrictionMixin(object):
self.apply_search_restriction(r) self.apply_search_restriction(r)
def apply_text_search_restriction(self, search): def apply_text_search_restriction(self, search):
search = unicode(search)
if not search: if not search:
self.search_restriction.setItemText(1, _('*Current search'))
self.search_restriction.setCurrentIndex(0) self.search_restriction.setCurrentIndex(0)
else: else:
self.search_restriction.setCurrentIndex(1) s = '*' + search
self.search_restriction.setItemText(1, 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('<p>' +
self.search_restriction_tooltip +
_(' or the search ') + "'" + search + "'</p>")
self._apply_search_restriction(search) self._apply_search_restriction(search)
def apply_search_restriction(self, i): def apply_search_restriction(self, i):
self.search_restriction.setItemText(1, _('*Current search'))
if i == 1: if i == 1:
restriction = unicode(self.search.currentText()) self.apply_text_search_restriction(unicode(self.search.currentText()))
if not restriction: elif i == 2 and unicode(self.search_restriction.currentText()).startswith('*'):
self.search_restriction.setCurrentIndex(0) self.apply_text_search_restriction(
else: unicode(self.search_restriction.currentText())[1:])
self.search_restriction.setItemText(1, restriction)
else: else:
r = unicode(self.search_restriction.currentText()) r = unicode(self.search_restriction.currentText())
if r is not None and r != '': if r is not None and r != '':
restriction = 'search:"%s"'%(r) restriction = 'search:"%s"'%(r)
else: else:
restriction = '' restriction = ''
self._apply_search_restriction(restriction) self._apply_search_restriction(restriction)
def _apply_search_restriction(self, restriction): def _apply_search_restriction(self, restriction):
self.saved_search.clear() self.saved_search.clear()

View File

@ -154,6 +154,13 @@ class AmazonKindleStore(StorePlugin):
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href) cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
if cover_img: if cover_img:
cover_url = cover_img[0] 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()')) title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))

View File

@ -11,7 +11,11 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>calibre Store Search</string> <string>Get Books</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
</property> </property>
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
<bool>true</bool> <bool>true</bool>
@ -58,8 +62,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>215</width> <width>170</width>
<height>116</height> <height>138</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -174,7 +178,9 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<resources/> <resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections> <connections>
<connection> <connection>
<sender>close</sender> <sender>close</sender>

View File

@ -70,16 +70,17 @@ class NPWebView(QWebView):
if ext not in BOOK_EXTENSIONS: if ext not in BOOK_EXTENSIONS:
if ext == 'acsm': if ext == 'acsm':
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
confirm(_('<p>You have selected to download an ebook that uses ' if not confirm('<p>' + _('This ebook is a DRMed EPUB file. '
'the ACSM format. ACSM files are not ebook files but ' 'You will be prompted to save this file to your '
'pointers ' 'computer. Once it is saved, open it with '
'<a href="http://www.adobe.com/products/digitaleditions/">' '<a href="http://www.adobe.com/products/digitaleditions/">'
'Adobe Digital Editions</a> (ADE) uses so it can download ' 'Adobe Digital Editions</a> (ADE).<p>ADE, in turn '
'the ebook and apply DRM to it. You will be prompted to save this ' 'will download the actual ebook, which will be a '
'file to your computer. Then open it using ADE. Once ADE has ' '.epub file. You can add this book to calibre '
'downloaded the actual ebook you can add the book to calibre ' 'using "Add Books" and selecting the file from '
'using Add and selecting the file from the ADE library folder.'), 'the ADE library folder.'),
'acsm_download', self) 'acsm_download', self):
return
home = os.path.expanduser('~') home = os.path.expanduser('~')
name = QFileDialog.getSaveFileName(self, name = QFileDialog.getSaveFileName(self,
_('File is not a supported ebook type. Save to disk?'), _('File is not a supported ebook type. Save to disk?'),

View File

@ -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.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image from calibre import fit_image
from calibre.ebooks import BOOK_EXTENSIONS 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.utils.config import prefs, XMLConfig, tweaks
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ 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) self.re.setCurrentIndex(0)
def do_test(self): def do_test(self):
from calibre.ebooks.metadata.meta import metadata_from_filename
try: try:
pat = self.pattern() pat = self.pattern()
except Exception as err: except Exception as err:

View File

@ -707,7 +707,10 @@ class ResultCache(SearchQueryParser): # {{{
for loc in location: # location is now an array of field indices for loc in location: # location is now an array of field indices
if loc == db_col['authors']: if loc == db_col['authors']:
### DB stores authors with commas changed to bars, so change query ### 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: else:
q = query q = query

View File

@ -15,7 +15,6 @@ from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
from calibre.ebooks.chardet import substitute_entites from calibre.ebooks.chardet import substitute_entites
from calibre.ebooks.oeb.base import XHTML_NS
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config import config_dir from calibre.utils.config import config_dir
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf 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 Generate description header from template
''' '''
from calibre.ebooks.oeb.base import XHTML_NS
def generate_html(): def generate_html():
args = dict( args = dict(
author=author, author=author,

View File

@ -10,8 +10,7 @@ Command line interface to the calibre database.
import sys, os, cStringIO, re import sys, os, cStringIO, re
from textwrap import TextWrapper from textwrap import TextWrapper
from calibre import terminal_controller, preferred_encoding, prints, \ from calibre import preferred_encoding, prints, isbytestring
isbytestring
from calibre.utils.config import OptionParser, prefs, tweaks from calibre.utils.config import OptionParser, prefs, tweaks
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.library.database2 import LibraryDatabase2 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, def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator,
prefix, subtitle='Books in the calibre database'): prefix, subtitle='Books in the calibre database'):
from calibre.constants import terminal_controller as tc
terminal_controller = tc()
if sort_by: if sort_by:
db.sort(sort_by, ascending) db.sort(sort_by, ascending)
if search_text: if search_text:
@ -1087,6 +1088,9 @@ def command_list_categories(args, dbpath):
fields = ['category', 'tag_name', 'count', 'rating'] fields = ['category', 'tag_name', 'count', 'rating']
def do_list(): def do_list():
from calibre.constants import terminal_controller as tc
terminal_controller = tc()
separator = ' ' separator = ' '
widths = list(map(lambda x : 0, fields)) widths = list(map(lambda x : 0, fields))
for i in data: for i in data:

View File

@ -15,7 +15,8 @@ from math import ceil
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
from calibre import prints 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.ebooks.metadata.opf2 import metadata_to_opf
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons 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.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError from calibre.library.sqlite import connect, IntegrityError
from calibre.library.prefs import DBPrefs 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.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.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
@ -853,6 +852,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.pubdate = row[fm['pubdate']] mi.pubdate = row[fm['pubdate']]
mi.uuid = row[fm['uuid']] mi.uuid = row[fm['uuid']]
mi.title_sort = row[fm['sort']] mi.title_sort = row[fm['sort']]
mi.book_size = row[fm['size']]
mi.last_modified = row[fm['last_modified']] mi.last_modified = row[fm['last_modified']]
formats = row[fm['formats']] formats = row[fm['formats']]
if not formats: if not formats:
@ -1378,13 +1378,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for (cat, dex, mult, is_comp) in md: for (cat, dex, mult, is_comp) in md:
if not book[dex]: if not book[dex]:
continue continue
tid_cat = tids[cat]
tcats_cat = tcategories[cat]
if not mult: if not mult:
val = book[dex] val = book[dex]
if is_comp: if is_comp:
item = tcategories[cat].get(val, None) item = tcats_cat.get(val, None)
if not item: if not item:
item = tag_class(val, val) item = tag_class(val, val)
tcategories[cat][val] = item tcats_cat[val] = item
item.c += 1 item.c += 1
item.id = val item.id = val
if rating > 0: if rating > 0:
@ -1392,11 +1394,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
item.rc += 1 item.rc += 1
continue continue
try: try:
(item_id, sort_val) = tids[cat][val] # let exceptions fly (item_id, sort_val) = tid_cat[val] # let exceptions fly
item = tcategories[cat].get(val, None) item = tcats_cat.get(val, None)
if not item: if not item:
item = tag_class(val, sort_val) item = tag_class(val, sort_val)
tcategories[cat][val] = item tcats_cat[val] = item
item.c += 1 item.c += 1
item.id_set.add(book[0]) item.id_set.add(book[0])
item.id = item_id item.id = item_id
@ -1410,21 +1412,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if is_comp: if is_comp:
vals = [v.strip() for v in vals if v.strip()] vals = [v.strip() for v in vals if v.strip()]
for val in vals: for val in vals:
if val not in tids: if val not in tid_cat:
tids[cat][val] = (val, val) tid_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
for val in vals: for val in vals:
try: try:
(item_id, sort_val) = tids[cat][val] # let exceptions fly (item_id, sort_val) = tid_cat[val] # let exceptions fly
item = tcategories[cat].get(val, None) item = tcats_cat.get(val, None)
if not item: if not item:
item = tag_class(val, sort_val) item = tag_class(val, sort_val)
tcategories[cat][val] = item tcats_cat[val] = item
item.c += 1 item.c += 1
item.id_set.add(book[0]) item.id_set.add(book[0])
item.id = item_id item.id = item_id
@ -2732,6 +2728,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_identifier(id_, 'isbn', isbn, notify=notify, commit=commit) self.set_identifier(id_, 'isbn', isbn, notify=notify, commit=commit)
def add_catalog(self, path, title): def add_catalog(self, path, title):
from calibre.ebooks.metadata.meta import get_metadata
format = os.path.splitext(path)[1][1:].lower() format = os.path.splitext(path)[1][1:].lower()
with lopen(path, 'rb') as stream: with lopen(path, 'rb') as stream:
matches = self.data.get_matches('title', '='+title) matches = self.data.get_matches('title', '='+title)
@ -2767,6 +2765,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def add_news(self, path, arg): def add_news(self, path, arg):
from calibre.ebooks.metadata.meta import get_metadata
format = os.path.splitext(path)[1][1:].lower() format = os.path.splitext(path)[1][1:].lower()
stream = path if hasattr(path, 'read') else lopen(path, 'rb') stream = path if hasattr(path, 'read') else lopen(path, 'rb')
stream.seek(0) stream.seek(0)
@ -3160,6 +3160,8 @@ books_series_link feeds
yield formats yield formats
def import_book_directory_multiple(self, dirpath, callback=None): def import_book_directory_multiple(self, dirpath, callback=None):
from calibre.ebooks.metadata.meta import metadata_from_formats
duplicates = [] duplicates = []
for formats in self.find_books_in_directory(dirpath, False): for formats in self.find_books_in_directory(dirpath, False):
mi = metadata_from_formats(formats) mi = metadata_from_formats(formats)
@ -3175,6 +3177,7 @@ books_series_link feeds
return duplicates return duplicates
def import_book_directory(self, dirpath, callback=None): def import_book_directory(self, dirpath, callback=None):
from calibre.ebooks.metadata.meta import metadata_from_formats
dirpath = os.path.abspath(dirpath) dirpath = os.path.abspath(dirpath)
formats = self.find_books_in_directory(dirpath, True) formats = self.find_books_in_directory(dirpath, True)
formats = list(formats)[0] formats = list(formats)[0]

View File

@ -35,7 +35,7 @@ category_icon_map = {
'custom:' : 'column.png', 'custom:' : 'column.png',
'user:' : 'tb_folder.png', 'user:' : 'tb_folder.png',
'search' : 'search.png', 'search' : 'search.png',
'identifiers': 'id_card.png' 'identifiers': 'identifiers.png'
} }

View File

@ -14,7 +14,6 @@ from calibre.utils.formatter import TemplateFormatter
from calibre.utils.filenames import shorten_components_to, supports_long_names, \ from calibre.utils.filenames import shorten_components_to, supports_long_names, \
ascii_filename ascii_filename
from calibre.ebooks.metadata.opf2 import metadata_to_opf 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.constants import preferred_encoding
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
@ -51,6 +50,23 @@ for x in FORMAT_ARG_DESCS:
FORMAT_ARGS[x] = '' 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): def config(defaults=None):
if defaults is None: if defaults is None:
c = Config('save_to_disk', _('Options to control saving to disk')) 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: for key in custom_metadata:
if key in format_args: if key in format_args:
cm = custom_metadata[key] cm = custom_metadata[key]
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
if cm['datatype'] == 'series': if cm['datatype'] == 'series':
format_args[key] = title_sort(format_args[key], order=tsorder) format_args[key] = title_sort(format_args[key], order=tsorder)
if key+'_index' in format_args: 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, def do_save_book_to_disk(id_, mi, cover, plugboards,
format_map, root, opts, length): format_map, root, opts, length):
from calibre.ebooks.metadata.meta import set_metadata
available_formats = [x.lower().strip() for x in format_map.keys()] available_formats = [x.lower().strip() for x in format_map.keys()]
if opts.formats == 'all': if opts.formats == 'all':
asked_formats = available_formats asked_formats = available_formats
@ -279,20 +295,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
written = False written = False
for fmt in formats: for fmt in formats:
global plugboard_save_to_disk_value, plugboard_any_format_value global plugboard_save_to_disk_value, plugboard_any_format_value
dev_name = plugboard_save_to_disk_value cpb = find_plugboard(plugboard_save_to_disk_value, fmt, plugboards)
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
# Leave this here for a while, in case problems arise. # Leave this here for a while, in case problems arise.
if cpb is not None: if cpb is not None:
prints('Save-to-disk using plugboard:', fmt, cpb) prints('Save-to-disk using plugboard:', fmt, cpb)

View File

@ -12,9 +12,14 @@ import cherrypy
from calibre import fit_image, guess_type from calibre import fit_image, guess_type
from calibre.utils.date import fromtimestamp from calibre.utils.date import fromtimestamp
from calibre.library.caches import SortKeyGenerator 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, \ from calibre.utils.magick.draw import save_cover_data_to, Image, \
thumbnail as generate_thumbnail thumbnail as generate_thumbnail
plugboard_content_server_value = 'content_server'
plugboard_content_server_formats = ['epub']
class CSSortKeyGenerator(SortKeyGenerator): class CSSortKeyGenerator(SortKeyGenerator):
def __init__(self, fields, fm, db_prefs): def __init__(self, fields, fm, db_prefs):
@ -183,16 +188,30 @@ class ContentServer(object):
if fmt is None: if fmt is None:
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format)) raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
if format == 'EPUB': 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 tempfile import TemporaryFile
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
raw = fmt.read() raw = fmt.read()
fmt = TemporaryFile() fmt = TemporaryFile()
fmt.write(raw) fmt.write(raw)
fmt.seek(0) fmt.seek(0)
set_metadata(fmt, self.db.get_metadata(id, index_is_id=True, set_metadata(fmt, newmi, 'epub')
get_cover=True),
'epub')
fmt.seek(0) fmt.seek(0)
mt = guess_type('dummy.'+format.lower())[0] mt = guess_type('dummy.'+format.lower())[0]
if mt is None: if mt is None:
mt = 'application/octet-stream' mt = 'application/octet-stream'

View File

@ -549,17 +549,6 @@ How do I run calibre from my USB stick?
A portable version of calibre is available at: `portableapps.com <http://portableapps.com/node/20518>`_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions <portablecalibre>`. A portable version of calibre is available at: `portableapps.com <http://portableapps.com/node/20518>`_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions <portablecalibre>`.
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? How do I run parts of |app| like news download and the content server on my own linux server?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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. * ``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 * ``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``. * ``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. * ``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``. * ``field(name)`` -- returns the metadata field named by ``name``.

Some files were not shown because too many files have changed in this diff Show More