Merge from trunk

This commit is contained in:
Charles Haley 2011-03-27 12:02:45 +01:00
commit 38d2bc0b00
197 changed files with 35378 additions and 27764 deletions

View File

@ -11,7 +11,8 @@ resources/localization
resources/images.qrc resources/images.qrc
resources/scripts.pickle resources/scripts.pickle
resources/ebook-convert-complete.pickle resources/ebook-convert-complete.pickle
resources/builtin_recipes.* resources/builtin_recipes.xml
resources/builtin_recipes.zip
setup/installer/windows/calibre/build.log setup/installer/windows/calibre/build.log
src/calibre/translations/.errors src/calibre/translations/.errors
src/cssutils/.svn/ src/cssutils/.svn/

View File

@ -19,6 +19,80 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.52
date: 2011-03-25
bug fixes:
- title: "Fixes a typo in 0.7.51 that broke the downloading of some news. Apologies."
tickets: [742840]
- version: 0.7.51
date: 2011-03-25
new features:
- title: "Conversion: Detect and remove fake page margins that are specified as a margin on (nearly) every paragraph."
description: "This can be turned off via an option under Structure Detection, in case it removes margins that should have been kept."
- title: "Windows build: All the python code and recipes are now put into zip files. This should decrease the amount of time the windows installer spends 'calculating free space'"
- title: "OSX and Linux: Add a setting in Preferences->Behavior to control the priority with which calibre worker processes run. This setting was already available on windows."
tickets: [741231]
- title: "Driver for HTC Thunderbolt, T-Mobile Optimus, Archos 43 and Blackberry OS6"
- title: "A new 'authors type' custom column"
- title: "When building calibre from source note that calibre now absolutely requires python >= 2.7"
- title: "Add the keyboard shortcut: Ctrl+Shift+R to restart calibre in debug mode"
bug fixes:
- title: "Fix dragging and dropping lots of books from the book list to the Tag Browser was broken"
- title: "Change the shebang in the calibre launcher script on linux to explicitly use python2 rather than python"
- title: "When adding formats do not corrupt the added file if the user tries to add an existing format to itself"
- title: "Fix drag and drop to add files that contain the # character in the filename"
- title: "Tag editor shouldn't add empty tags"
tickets: [740890]
- title: "MOBI Input: Handle MOBI files that have a too large 'number of records' field in their headers."
tickets: [740713]
- title: "News download: Update RSS feedparser module to latest version"
- title: "Various fixes to the zipfile module in calibre to handle 64 bit zipfiles and bring it up to date with the zip file module in the python stdlib"
- title: "News download: Handle titles with ASCII control codes in them."
tickets: [739322]
- title: "Make search hierarchies show simple names instead of compound ones."
- title: "Fix commas in author names being converted to pipe symbols in the book details window"
- title: "Fix PocketBook can't always find epub cover image to create thumbnail"
tickets: [9445]
improved recipes:
- "168 ora"
- "LWN weekly"
- Christian Science Monitor
- Washington Post
- West Hawaii Today
new recipes:
- title: "Planet KDE"
author: Riccardo Iaconelli
- title: "HVG"
author: Istvan Papp
- title: "Caijing Magazine"
auhtor: Eric Chen
- version: 0.7.50 - version: 0.7.50
date: 2011-03-18 date: 2011-03-18

View File

@ -0,0 +1,83 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Cracked(BasicNewsRecipe):
title = u'Cracked.com'
__author__ = u'Nudgenudge'
language = 'en'
description = 'America''s Only Humor and Video Site, since 1958'
publisher = 'Cracked'
category = 'comedy, lists'
oldest_article = 2
delay = 10
max_articles_per_feed = 2
no_stylesheets = True
encoding = 'cp1252'
remove_javascript = True
use_embedded_content = False
INDEX = u'http://www.cracked.com'
extra_css = """
.pageheader_type{font-size: x-large; font-weight: bold; color: #828D74}
.pageheader_title{font-size: xx-large; color: #394128}
.pageheader_byline{font-size: small; font-weight: bold; color: #394128}
.score_bg {display: inline; width: 100%; margin-bottom: 2em}
.score_column_1{ padding-left: 10px; font-size: small; width: 50%}
.score_column_2{ padding-left: 10px; font-size: small; width: 50%}
.score_column_3{ padding-left: 10px; font-size: small; width: 50%}
.score_header{font-size: large; color: #50544A}
.bodytext{display: block}
body{font-family: Helvetica,Arial,sans-serif}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
keep_only_tags = [
dict(name='div', attrs={'class':['Column1']})
]
feeds = [(u'Articles', u'http://feeds.feedburner.com/CrackedRSS')]
def get_article_url(self, article):
return article.get('guid', None)
def cleanup_page(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
for div_to_remove in soup.findAll('div', attrs={'id':['googlead_1','fb-like-article','comments_section']}):
div_to_remove.extract()
for div_to_remove in soup.findAll('div', attrs={'class':['share_buttons_col_1','GenericModule1']}):
div_to_remove.extract()
for div_to_remove in soup.findAll('div', attrs={'class':re.compile("prev_next")}):
div_to_remove.extract()
for ul_to_remove in soup.findAll('ul', attrs={'class':['Nav6']}):
ul_to_remove.extract()
for image in soup.findAll('img', attrs={'alt': 'article image'}):
image.extract()
def append_page(self, soup, appendtag, position):
pager = soup.find('a',attrs={'class':'next_arrow_active'})
if pager:
nexturl = self.INDEX + pager['href']
soup2 = self.index_to_soup(nexturl)
texttag = soup2.find('div', attrs={'class':re.compile("userStyled")})
newpos = len(texttag.contents)
self.append_page(soup2,texttag,newpos)
texttag.extract()
self.cleanup_page(appendtag)
appendtag.insert(position,texttag)
else:
self.cleanup_page(appendtag)
def preprocess_html(self, soup):
self.append_page(soup, soup.body, 3)
return self.adeify_images(soup)

View File

@ -1,33 +1,51 @@
#!/usr/bin/env python # -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai import re
from __future__ import with_statement from calibre.web.feeds.recipes import BasicNewsRecipe
__license__ = 'GPL v3' class hu168ora(BasicNewsRecipe):
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' title = u'168 óra'
__docformat__ = 'restructuredtext en' __author__ = u'István Papp'
description = u'A 168 óra friss hírei'
timefmt = ' [%Y. %b. %d., %a.]'
oldest_article = 7
language = 'hu'
from calibre.web.feeds.news import BasicNewsRecipe max_articles_per_feed = 100
no_stylesheets = True
class H168(BasicNewsRecipe): use_embedded_content = False
title = u'168\xf3ra' encoding = 'utf8'
oldest_article = 4 publisher = u'Telegráf Kiadó'
max_articles_per_feed = 50 category = u'news, hírek, 168'
language = 'hu' extra_css = 'body{ font-family: Verdana,Helvetica,Arial,sans-serif }'
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
__author__ = 'Ezmegaz' keep_only_tags = [
dict(id='cikk_fejlec')
feeds = [(u'Itthon', ,dict(id='cikk_torzs')
u'http://www.168ora.hu/static/rss/cikkek_itthon.xml'), (u'Gl\xf3busz', ]
u'http://www.168ora.hu/static/rss/cikkek_globusz.xml'), (u'Punch', # remove_tags_before = dict(id='cikk_fejlec')
u'http://www.168ora.hu/static/rss/cikkek_punch.xml'), (u'Arte', # remove_tags_after = dict(id='szoveg')
u'http://www.168ora.hu/static/rss/cikkek_arte.xml'), (u'Buxa', remove_tags = [
u'http://www.168ora.hu/static/rss/cikkek_buxa.xml'), (u'Sebess\xe9g', dict(id='box_toolbar')
u'http://www.168ora.hu/static/rss/cikkek_sebesseg.xml'), (u'Tud\xe1s', ,dict(id='text')
u'http://www.168ora.hu/static/rss/cikkek_tudas.xml'), (u'Sport', ]
u'http://www.168ora.hu/static/rss/cikkek_sport.xml'), (u'V\xe9lem\xe9ny', remove_javascript = True
u'http://www.168ora.hu/static/rss/cikkek_velemeny.xml'), (u'Dolce Vita', remove_empty_feeds = True
u'http://www.168ora.hu/static/rss/cikkek_dolcevita.xml'), (u'R\xe1di\xf3',
u'http://www.168ora.hu/static/rss/radio.xml')]
feeds = [
(u'Itthon', u'http://www.168ora.hu/static/rss/cikkek_itthon.xml')
,(u'Glóbusz', u'http://www.168ora.hu/static/rss/cikkek_globusz.xml')
,(u'Punch', u'http://www.168ora.hu/static/rss/cikkek_punch.xml')
,(u'Arte', u'http://www.168ora.hu/static/rss/cikkek_arte.xml')
,(u'Buxa', u'http://www.168ora.hu/static/rss/cikkek_buxa.xml')
,(u'Sebesség', u'http://www.168ora.hu/static/rss/cikkek_sebesseg.xml')
,(u'Tudás', u'http://www.168ora.hu/static/rss/cikkek_tudas.xml')
,(u'Sport', u'http://www.168ora.hu/static/rss/cikkek_sport.xml')
,(u'Vélemény', u'http://www.168ora.hu/static/rss/cikkek_velemeny.xml')
,(u'Dolce Vita', u'http://www.168ora.hu/static/rss/cikkek_dolcevita.xml')
# ,(u'Rádió', u'http://www.168ora.hu/static/rss/radio.xml')
]
def print_version(self, url):
url += '?print=1'
return url

View File

@ -1,4 +1,3 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
@ -10,10 +9,12 @@ class Handelsblatt(BasicNewsRecipe):
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 = []
keep_only_tags.append(dict(name = 'div', attrs = {'class': 'structOneCol'})) keep_only_tags = (dict(name = 'div', attrs = {'class': ['hcf-detail-abstract hcf-teaser ajaxify','hcf-detail','hcf-author-wrapper']}))
keep_only_tags.append(dict(name = 'div', attrs = {'id': 'fullText'})) # keep_only_tags.append(dict(name = 'div', attrs = {'id': 'fullText'}))
remove_tags = [dict(name='img', attrs = {'src': 'http://www.handelsblatt.com/images/icon/loading.gif'})] 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,14 +29,16 @@ class Handelsblatt(BasicNewsRecipe):
(u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs') (u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs')
] ]
extra_css = ''' extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} .hcf-headline {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-overline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;} .hcf-exclusive {font-family:Arial,Helvetica,sans-serif; font-style:italic;font-weight:bold; margin-right:5pt;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;} p{font-family:Arial,Helvetica,sans-serif;}
.hcf-location-mark{font-weight:bold; margin-right:5pt;}
.MsoNormal{font-family:Helvetica,Arial,sans-serif;}
.hcf-author-wrapper{font-style:italic;}
.hcf-article-date{font-size:x-small;}
.hcf-caption {font-style:italic;font-size:small;}
img {align:left;}
''' '''
def print_version(self, url):
m = re.search('(?<=;)[0-9]*', url)
return u'http://www.handelsblatt.com/_b=' + str(m.group(0)) + ',_p=21,_t=ftprint,doc_page=0;printpage'

44
recipes/hvg.recipe Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class HVG(BasicNewsRecipe):
title = 'HVG.HU'
__author__ = u'István Papp'
description = u'Friss hírek a HVG-től'
timefmt = ' [%Y. %b. %d., %a.]'
oldest_article = 4
language = 'hu'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
publisher = 'HVG Online'
category = u'news, hírek, hvg'
extra_css = 'body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
remove_tags_before = dict(id='pg-content')
remove_javascript = True
remove_empty_feeds = True
feeds = [
(u'Itthon', u'http://hvg.hu/rss/itthon')
,(u'Világ', u'http://hvg.hu/rss/vilag')
,(u'Gazdaság', u'http://hvg.hu/rss/gazdasag')
,(u'IT | Tudomány', u'http://hvg.hu/rss/tudomany')
,(u'Panoráma', u'http://hvg.hu/rss/Panorama')
,(u'Karrier', u'http://hvg.hu/rss/karrier')
,(u'Gasztronómia', u'http://hvg.hu/rss/gasztronomia')
,(u'Helyi érték', u'http://hvg.hu/rss/helyiertek')
,(u'Kultúra', u'http://hvg.hu/rss/kultura')
,(u'Cégautó', u'http://hvg.hu/rss/cegauto')
,(u'Vállalkozó szellem', u'http://hvg.hu/rss/kkv')
,(u'Egészség', u'http://hvg.hu/rss/egeszseg')
,(u'Vélemény', u'http://hvg.hu/rss/velemeny')
,(u'Sport', u'http://hvg.hu/rss/sport')
]
def print_version(self, url):
return url.replace ('#rss', '/print')

View File

@ -23,6 +23,11 @@ class WeeklyLWN(BasicNewsRecipe):
remove_tags_after = dict(attrs={'class':'ArticleText'}) remove_tags_after = dict(attrs={'class':'ArticleText'})
remove_tags = [dict(name=['h2', 'form'])] remove_tags = [dict(name=['h2', 'form'])]
preprocess_regexps = [
# Remove the <hr> and "Log in to post comments"
(re.compile(r'<hr.*?comments[)]', re.DOTALL), lambda m: ''),
]
conversion_options = { 'linearize_tables' : True } conversion_options = { 'linearize_tables' : True }
oldest_article = 7.0 oldest_article = 7.0
@ -40,15 +45,15 @@ class WeeklyLWN(BasicNewsRecipe):
def parse_index(self): def parse_index(self):
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
index_url = 'http://lwn.net/current/bigpage' index_url = 'http://lwn.net/current/bigpage?format=printable'
else: else:
index_url = 'http://lwn.net/free/bigpage' index_url = 'http://lwn.net/free/bigpage?format=printable'
soup = self.index_to_soup(index_url) soup = self.index_to_soup(index_url)
body = soup.body body = soup.body
articles = {} articles = {}
ans = [] ans = []
url_re = re.compile('^http://lwn.net/Articles/') url_re = re.compile('^/Articles/')
while True: while True:
tag_title = body.findNext(name='p', attrs={'class':'SummaryHL'}) tag_title = body.findNext(name='p', attrs={'class':'SummaryHL'})
@ -91,7 +96,7 @@ class WeeklyLWN(BasicNewsRecipe):
article = dict( article = dict(
title=tag_title.string, title=tag_title.string,
url=tag_url['href'].split('#')[0], url= 'http://lwn.net' + tag_url['href'].split('#')[0] + '?format=printable',
description='', content='', date='') description='', content='', date='')
articles[section].append(article) articles[section].append(article)

11
recipes/planet_kde.recipe Normal file
View File

@ -0,0 +1,11 @@
from calibre.web.feeds.news import AutomaticNewsRecipe
class BasicUserRecipe1300864518(AutomaticNewsRecipe):
title = u'KDE News'
language = 'en'
__author__ = 'Riccardo Iaconelli'
oldest_article = 10
max_articles_per_feed = 100
feeds = [(u'Planet KDE', u'http://planetkde.org/rss20.xml'), (u'Got the Dot?', u'http://dot.kde.org/rss.xml')]

View File

@ -1,7 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1291143841(BasicNewsRecipe): class AdvancedUserRecipe1291143841(BasicNewsRecipe):
title = u'Poughkeepsipe Journal' title = u'Poughkeepsie Journal'
language = 'en' language = 'en'
__author__ = 'weebl' __author__ = 'weebl'
oldest_article = 7 oldest_article = 7

View File

@ -12,7 +12,7 @@ from setup import Command, islinux, isfreebsd, basenames, modules, functions, \
__appname__, __version__ __appname__, __version__
HEADER = '''\ HEADER = '''\
#!/usr/bin/env python #!/usr/bin/env python2
""" """
This is the standard runscript for all of calibre's tools. This is the standard runscript for all of calibre's tools.

View File

@ -14,7 +14,7 @@ from setup.build_environment import HOST, PROJECT
BASE_RSYNC = ['rsync', '-avz', '--delete'] BASE_RSYNC = ['rsync', '-avz', '--delete']
EXCLUDES = [] EXCLUDES = []
for x in [ for x in [
'src/calibre/plugins', 'src/calibre/manual', 'src/calibre/trac', 'recipes', 'src/calibre/plugins', 'src/calibre/manual', 'src/calibre/trac',
'.bzr', '.build', '.svn', 'build', 'dist', 'imgsrc', '*.pyc', '*.pyo', '*.swp', '.bzr', '.build', '.svn', 'build', 'dist', 'imgsrc', '*.pyc', '*.pyo', '*.swp',
'*.swo', 'format_docs']: '*.swo', 'format_docs']:
EXCLUDES.extend(['--exclude', x]) EXCLUDES.extend(['--exclude', x])

View File

@ -154,9 +154,9 @@
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" <CustomAction Id="LaunchApplication" BinaryKey="WixCA"
DllEntry="WixShellExec" Impersonate="yes"/> DllEntry="WixShellExec" Impersonate="yes"/>
<!--<InstallUISequence> <InstallUISequence>
<FileCost Suppress="yes" /> <FileCost Suppress="yes" />
</InstallUISequence>--> </InstallUISequence>
</Product> </Product>
</Wix> </Wix>

View File

@ -93,9 +93,11 @@ class UploadToGoogleCode(Command): # {{{
ext = os.path.splitext(fname)[1][1:] ext = os.path.splitext(fname)[1][1:]
op = 'OpSys-'+{'msi':'Windows','dmg':'OSX','bz2':'Linux','gz':'All'}[ext] op = 'OpSys-'+{'msi':'Windows','dmg':'OSX','bz2':'Linux','gz':'All'}[ext]
desc = installer_description(fname) desc = installer_description(fname)
start = time.time()
path = self.upload(os.path.abspath(fname), desc, path = self.upload(os.path.abspath(fname), desc,
labels=[typ, op, 'Featured']) labels=[typ, op, 'Featured'])
self.info('\tUploaded to:', path) self.info('\tUploaded to:', path, 'in', int(time.time() - start),
'seconds')
return path return path
def run(self, opts): def run(self, opts):
@ -248,10 +250,13 @@ class UploadToSourceForge(Command): # {{{
def upload_installers(self): def upload_installers(self):
for x in installers(): for x in installers():
if not os.path.exists(x): continue if not os.path.exists(x): continue
start = time.time()
self.info('Uploading', x) self.info('Uploading', x)
check_call(['rsync', '-v', '-e', 'ssh -x', x, check_call(['rsync', '-v', '-e', 'ssh -x', x,
'%s,%s@frs.sourceforge.net:%s'%(self.USERNAME, self.PROJECT, '%s,%s@frs.sourceforge.net:%s'%(self.USERNAME, self.PROJECT,
self.rdir+'/')]) self.rdir+'/')])
print 'Uploaded in', int(time.time() - start), 'seconds'
print ('\n')
def run(self, opts): def run(self, opts):
self.opts = opts self.opts = opts

View File

@ -99,7 +99,7 @@ def sanitize_file_name_unicode(name, substitute='_'):
**WARNING:** This function also replaces path separators, so only pass file names **WARNING:** This function also replaces path separators, so only pass file names
and not full paths to it. and not full paths to it.
''' '''
if not isinstance(name, unicode): if isbytestring(name):
return sanitize_file_name(name, substitute=substitute, as_unicode=True) return sanitize_file_name(name, substitute=substitute, as_unicode=True)
chars = [substitute if c in _filename_sanitize_unicode else c for c in chars = [substitute if c in _filename_sanitize_unicode else c for c in
name] name]
@ -115,6 +115,14 @@ def sanitize_file_name_unicode(name, substitute='_'):
one = '_' + one[1:] one = '_' + one[1:]
return one return one
def sanitize_file_name2(name, substitute='_'):
'''
Sanitize filenames removing invalid chars. Keeps unicode names as unicode
and bytestrings as bytestrings
'''
if isbytestring(name):
return sanitize_file_name(name, substitute=substitute)
return sanitize_file_name_unicode(name, substitute=substitute)
def prints(*args, **kwargs): def prints(*args, **kwargs):
''' '''
@ -162,8 +170,8 @@ def prints(*args, **kwargs):
except: except:
file.write(repr(arg)) file.write(repr(arg))
if i != len(args)-1: if i != len(args)-1:
file.write(sep) file.write(bytes(sep))
file.write(end) file.write(bytes(end))
class CommandLineError(Exception): class CommandLineError(Exception):
pass pass
@ -270,12 +278,15 @@ def get_parsed_proxy(typ='http', debug=True):
def random_user_agent(): def random_user_agent():
choices = [ choices = [
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)',
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)' 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19',
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)' 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19' 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.14) Gecko/20080409 Camino/1.6 (like Firefox/2.0.0.14)',
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.1) Gecko/20060118 Camino/1.0b2+',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.78 Safari/532.5',
'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
] ]
return choices[random.randint(0, len(choices)-1)] return choices[random.randint(0, len(choices)-1)]

View File

@ -2,10 +2,10 @@ __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__ = 'calibre'
__version__ = '0.7.50' __version__ = '0.7.52'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re, importlib
_ver = __version__.split('.') _ver = __version__.split('.')
_ver = [int(re.search(r'(\d+)', x).group(1)) for x in _ver] _ver = [int(re.search(r'(\d+)', x).group(1)) for x in _ver]
numeric_version = tuple(_ver) numeric_version = tuple(_ver)
@ -33,10 +33,10 @@ try:
except: except:
preferred_encoding = 'utf-8' preferred_encoding = 'utf-8'
win32event = __import__('win32event') if iswindows else None win32event = importlib.import_module('win32event') if iswindows else None
winerror = __import__('winerror') if iswindows else None winerror = importlib.import_module('winerror') if iswindows else None
win32api = __import__('win32api') if iswindows else None win32api = importlib.import_module('win32api') if iswindows else None
fcntl = None if iswindows else __import__('fcntl') 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'
@ -74,8 +74,8 @@ if plugins is None:
(['winutil'] if iswindows else []) + \ (['winutil'] if iswindows else []) + \
(['usbobserver'] if isosx else []): (['usbobserver'] if isosx else []):
try: try:
p, err = __import__(plugin), '' p, err = importlib.import_module(plugin), ''
except Exception, err: except Exception as err:
p = None p = None
err = str(err) err = str(err)
plugins[plugin] = (p, err) plugins[plugin] = (p, err)

View File

@ -2,11 +2,24 @@ from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, zipfile import os, sys, zipfile, importlib
from calibre.constants import numeric_version from calibre.constants import numeric_version, iswindows, isosx
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
platform = 'linux'
if iswindows:
platform = 'windows'
elif isosx:
platform = 'osx'
class PluginNotFound(ValueError):
pass
class InvalidPlugin(ValueError):
pass
class Plugin(object): # {{{ class Plugin(object): # {{{
''' '''
@ -517,7 +530,7 @@ class InterfaceActionBase(Plugin): # {{{
This method must return the actual interface action plugin object. This method must return the actual interface action plugin object.
''' '''
mod, cls = self.actual_plugin.split(':') mod, cls = self.actual_plugin.split(':')
return getattr(__import__(mod, fromlist=['1'], level=0), cls)(gui, return getattr(importlib.import_module(mod), cls)(gui,
self.site_customization) self.site_customization)
# }}} # }}}
@ -575,7 +588,7 @@ class PreferencesPlugin(Plugin): # {{{
base, _, wc = self.config_widget.partition(':') base, _, wc = self.config_widget.partition(':')
if not wc: if not wc:
wc = 'ConfigWidget' wc = 'ConfigWidget'
base = __import__(base, fromlist=[1]) base = importlib.import_module(base)
widget = getattr(base, wc) widget = getattr(base, wc)
return widget(parent) return widget(parent)

View File

@ -1032,7 +1032,8 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
# New metadata download plugins {{{ # New metadata download plugins {{{
from calibre.ebooks.metadata.sources.google import GoogleBooks from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
plugins += [GoogleBooks, Amazon] plugins += [GoogleBooks, Amazon, OpenLibrary]
# }}} # }}}

View File

@ -2,17 +2,16 @@ from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys, re import os, shutil, traceback, functools, sys
from contextlib import closing
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
MetadataReaderPlugin, MetadataWriterPlugin, \ MetadataReaderPlugin, MetadataWriterPlugin,
InterfaceActionBase as InterfaceAction, \ InterfaceActionBase as InterfaceAction,
PreferencesPlugin PreferencesPlugin, platform, InvalidPlugin)
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.zipplugin import loader
from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.builtins import plugins as builtin_plugins
from calibre.constants import numeric_version as version, iswindows, isosx
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload from calibre.ebooks.metadata.covers import CoverDownload
@ -22,14 +21,6 @@ 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
platform = 'linux'
if iswindows:
platform = 'windows'
elif isosx:
platform = 'osx'
from zipfile import ZipFile
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'))
@ -42,11 +33,6 @@ def _config():
config = _config() config = _config()
class InvalidPlugin(ValueError):
pass
class PluginNotFound(ValueError):
pass
def find_plugin(name): def find_plugin(name):
for plugin in _initialized_plugins: for plugin in _initialized_plugins:
@ -60,38 +46,7 @@ def load_plugin(path_to_zip_file): # {{{
:return: A :class:`Plugin` instance. :return: A :class:`Plugin` instance.
''' '''
#print 'Loading plugin from', path_to_zip_file return loader.load(path_to_zip_file)
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound
with closing(ZipFile(path_to_zip_file)) as zf:
for name in zf.namelist():
if name.lower().endswith('plugin.py'):
locals = {}
raw = zf.read(name)
lines, encoding = raw.splitlines(), 'utf-8'
cr = re.compile(r'coding[:=]\s*([-\w.]+)')
raw = []
for l in lines[:2]:
match = cr.search(l)
if match is not None:
encoding = match.group(1)
else:
raw.append(l)
raw += lines[2:]
raw = '\n'.join(raw)
raw = raw.decode(encoding)
raw = re.sub('\r\n', '\n', raw)
exec raw in locals
for x in locals.values():
if isinstance(x, type) and issubclass(x, Plugin) and \
x.name != 'Trivial Plugin':
if x.minimum_calibre_version > version or \
platform not in x.supported_platforms:
continue
return x
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
# }}} # }}}

View File

@ -0,0 +1,196 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, zipfile, posixpath, importlib, threading, re, imp, sys
from collections import OrderedDict
from calibre.customize import (Plugin, numeric_version, platform,
InvalidPlugin, PluginNotFound)
# PEP 302 based plugin loading mechanism, works around the bug in zipimport in
# python 2.x that prevents importing from zip files in locations whose paths
# have non ASCII characters
class PluginLoader(object):
def __init__(self):
self.loaded_plugins = {}
self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def _get_actual_fullname(self, fullname):
parts = fullname.split('.')
if parts[0] == 'calibre_plugins':
if len(parts) == 1:
return parts[0], None
plugin_name = parts[1]
with self._lock:
names = self.loaded_plugins.get(plugin_name, None)[1]
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
fullname = '.'.join(parts[2:])
if not fullname:
fullname = '__init__'
if fullname in names:
return fullname, plugin_name
if fullname+'.__init__' in names:
return fullname+'.__init__', plugin_name
return None, None
def find_module(self, fullname, path=None):
fullname, plugin_name = self._get_actual_fullname(fullname)
if fullname is None and plugin_name is None:
return None
return self
def load_module(self, fullname):
import_name, plugin_name = self._get_actual_fullname(fullname)
if import_name is None and plugin_name is None:
raise ImportError('No plugin named %r is loaded'%fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<calibre Plugin Loader>"
mod.__loader__ = self
if import_name.endswith('.__init__') or import_name in ('__init__',
'calibre_plugins'):
# We have a package
mod.__path__ = []
if plugin_name is not None:
# We have some actual code to load
with self._lock:
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
zinfo = names.get(import_name, None)
if zinfo is None:
raise ImportError('Plugin %r has no module named %r' %
(plugin_name, import_name))
with zipfile.ZipFile(zfp) as zf:
code = zf.read(zinfo)
compiled = compile(code, 'import_name', 'exec', dont_inherit=True)
exec compiled in mod.__dict__
return mod
def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file)
with zipfile.ZipFile(path_to_zip_file) as zf:
plugin_name = self._locate_code(zf, path_to_zip_file)
try:
ans = None
m = importlib.import_module(
'calibre_plugins.%s'%plugin_name)
for obj in m.__dict__.itervalues():
if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin':
ans = obj
break
if ans is None:
raise InvalidPlugin('No plugin class found in %r:%r'%(
path_to_zip_file, plugin_name))
if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin(
'The plugin at %r needs a version of calibre >= %r' %
(path_to_zip_file, '.'.join(map(str,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms:
raise InvalidPlugin(
'The plugin at %r cannot be used on %s' %
(path_to_zip_file, platform))
return ans
except:
with self._lock:
del self.loaded_plugins[plugin_name]
raise
def _locate_code(self, zf, path_to_zip_file):
names = [x if isinstance(x, unicode) else x.decode('utf-8') for x in
zf.namelist()]
names = [x[1:] if x[0] == '/' else x for x in names]
plugin_name = None
for name in names:
name, ext = posixpath.splitext(name)
if name.startswith('plugin-import-name-') and ext == '.txt':
plugin_name = name.rpartition('-')[-1]
if plugin_name is None:
c = 0
while True:
c += 1
plugin_name = 'dummy%d'%c
if plugin_name not in self.loaded_plugins:
break
else:
if plugin_name in self.loaded_plugins:
raise InvalidPlugin((
'The plugin in %r uses an import name %r that is already'
' used by another plugin') % (path_to_zip_file, plugin_name))
if self._identifier_pat.match(plugin_name) is None:
raise InvalidPlugin((
'The plugin at %r uses an invalid import name: %r' %
(path_to_zip_file, plugin_name)))
pynames = [x for x in names if x.endswith('.py')]
candidates = [posixpath.dirname(x) for x in pynames if
x.endswith('/__init__.py')]
candidates.sort(key=lambda x: x.count('/'))
valid_packages = set()
for candidate in candidates:
parts = candidate.split('/')
parent = '.'.join(parts[:-1])
if parent and parent not in valid_packages:
continue
valid_packages.add('.'.join(parts))
names = OrderedDict()
for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1])
if package and package not in valid_packages:
continue
name = '.'.join(parts)
names[name] = zf.getinfo(candidate)
# Legacy plugins
if '__init__' not in names:
for name in list(names.iterkeys()):
if '.' not in name and name.endswith('plugin'):
names['__init__'] = names[name]
break
if '__init__' not in names:
raise InvalidPlugin(('The plugin in %r is invalid. It does not '
'contain a top-level __init__.py file')
% path_to_zip_file)
with self._lock:
self.loaded_plugins[plugin_name] = (path_to_zip_file, names)
return plugin_name
loader = PluginLoader()
sys.meta_path.insert(0, loader)

View File

@ -27,6 +27,7 @@ class ANDROID(USBMS):
0xc97 : [0x226], 0xc97 : [0x226],
0xc99 : [0x0100], 0xc99 : [0x0100],
0xca3 : [0x100], 0xca3 : [0x100],
0xca4 : [0x226],
}, },
# Eken # Eken

View File

@ -9,7 +9,7 @@ import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, t
from calibre.constants import __appname__, __version__, DEBUG from calibre.constants import __appname__, __version__, DEBUG
from calibre import fit_image from calibre import fit_image
from calibre.constants import isosx, iswindows from calibre.constants import isosx, iswindows
from calibre.devices.errors import UserFeedback from calibre.devices.errors import OpenFeedback, UserFeedback
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import BeautifulSoup
@ -23,6 +23,7 @@ from calibre.utils.date import now, parse_date
from calibre.utils.logging import Log from calibre.utils.logging import Log
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
from PIL import Image as PILImage from PIL import Image as PILImage
from lxml import etree from lxml import etree
@ -41,7 +42,29 @@ class DriverBase(DeviceConfig, DevicePlugin):
# Needed for config_widget to work # Needed for config_widget to work
FORMATS = ['epub', 'pdf'] FORMATS = ['epub', 'pdf']
USER_CAN_ADD_NEW_FORMATS = False USER_CAN_ADD_NEW_FORMATS = False
SUPPORTS_SUB_DIRS = True # To enable second checkbox in customize widget
# Hide the standard customization widgets
SUPPORTS_SUB_DIRS = False
MUST_READ_METADATA = True
SUPPORTS_USE_AUTHOR_SORT = False
EXTRA_CUSTOMIZATION_MESSAGE = [
_('Use Series as Category in iTunes/iBooks') +
':::'+_('Enable to use the series name as the iTunes Genre, '
'iBooks Category'),
_('Cache covers from iTunes/iBooks') +
':::' +
_('Enable to cache and display covers from iTunes/iBooks'),
_("Skip 'Connect to iTunes' recommendation") +
':::' +
_("Enable to skip the 'Connect to iTunes' recommendation dialog")
]
EXTRA_CUSTOMIZATION_DEFAULT = [
True,
True,
False,
]
@classmethod @classmethod
def _config_base_name(cls): def _config_base_name(cls):
@ -97,6 +120,11 @@ class ITUNES(DriverBase):
#: The version of this plugin as a 3-tuple (major, minor, revision) #: The version of this plugin as a 3-tuple (major, minor, revision)
version = (0,9,0) version = (0,9,0)
# EXTRA_CUSTOMIZATION_MESSAGE indexes
USE_SERIES_AS_CATEGORY = 0
CACHE_COVERS = 1
SKIP_CONNECT_TO_ITUNES_DIALOG = 2
OPEN_FEEDBACK_MESSAGE = _( OPEN_FEEDBACK_MESSAGE = _(
'Apple device detected, launching iTunes, please wait ...') 'Apple device detected, launching iTunes, please wait ...')
BACKLOADING_ERROR_MESSAGE = _( BACKLOADING_ERROR_MESSAGE = _(
@ -295,7 +323,7 @@ class ITUNES(DriverBase):
if not oncard: if not oncard:
if DEBUG: if DEBUG:
self.log.info("ITUNES:books():") self.log.info("ITUNES:books():")
if self.settings().use_subdirs: if self.settings().extra_customization[self.CACHE_COVERS]:
self.log.info(" Cover fetching/caching enabled") self.log.info(" Cover fetching/caching enabled")
else: else:
self.log.info(" Cover fetching/caching disabled") self.log.info(" Cover fetching/caching disabled")
@ -558,10 +586,6 @@ class ITUNES(DriverBase):
# Turn off the Save template # Turn off the Save template
cw.opt_save_template.setVisible(False) cw.opt_save_template.setVisible(False)
cw.label.setVisible(False) cw.label.setVisible(False)
# Repurpose the metadata checkbox
cw.opt_read_metadata.setText(_("Use Series as Category in iTunes/iBooks"))
# Repurpose the use_subdirs checkbox
cw.opt_use_subdirs.setText(_("Cache covers from iTunes/iBooks"))
return cw return cw
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
@ -718,6 +742,19 @@ class ITUNES(DriverBase):
if DEBUG: if DEBUG:
self.log.info("ITUNES.open()") self.log.info("ITUNES.open()")
# Display a dialog recommending using 'Connect to iTunes'
if False and not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]:
raise OpenFeedback('<p>' + ('Click the "Connect/Share" button and choose'
' "Connect to iTunes" to send books from your calibre library'
' to your Apple iDevice.<p>For more information, see '
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">'
'Calibre + Apple iDevices FAQ</a>.<p>'
'After following the Quick Start steps outlined in the FAQ, '
'restart calibre.'))
if DEBUG:
self.log.info(" advanced user mode, directly connecting to iDevice")
# Confirm/create thumbs archive # Confirm/create thumbs archive
if not os.path.exists(self.cache_dir): if not os.path.exists(self.cache_dir):
if DEBUG: if DEBUG:
@ -1787,9 +1824,7 @@ class ITUNES(DriverBase):
as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation
''' '''
# self.settings().use_subdirs is a repurposed DeviceConfig field if not self.settings().extra_customization[self.CACHE_COVERS]:
# We're using it to skip fetching/caching covers to speed things up
if not self.settings().use_subdirs:
thumb_data = None thumb_data = None
return thumb_data return thumb_data
@ -2673,8 +2708,7 @@ class ITUNES(DriverBase):
# Set genre from series if available, else first alpha tag # Set genre from series if available, else first alpha tag
# Otherwise iTunes grabs the first dc:subject from the opf metadata # Otherwise iTunes grabs the first dc:subject from the opf metadata
# self.settings().read_metadata is used as a surrogate for "Use Series name as Genre" if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]:
if metadata_x.series and self.settings().read_metadata:
if DEBUG: if DEBUG:
self.log.info(" ITUNES._update_iTunes_metadata()") self.log.info(" ITUNES._update_iTunes_metadata()")
self.log.info(" using Series name as Genre") self.log.info(" using Series name as Genre")
@ -2716,7 +2750,7 @@ class ITUNES(DriverBase):
elif metadata_x.tags is not None: elif metadata_x.tags is not None:
if DEBUG: if DEBUG:
self.log.info(" %susing Tag as Genre" % self.log.info(" %susing Tag as Genre" %
"no Series name available, " if self.settings().read_metadata else '') "no Series name available, " if self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY] else '')
for tag in metadata_x.tags: for tag in metadata_x.tags:
if self._is_alpha(tag[0]): if self._is_alpha(tag[0]):
if lb_added: if lb_added:
@ -2768,7 +2802,7 @@ class ITUNES(DriverBase):
# Otherwise iBooks uses first <dc:subject> from opf # Otherwise iBooks uses first <dc:subject> from opf
# iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12) # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12)
if metadata_x.series and self.settings().read_metadata: if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]:
if DEBUG: if DEBUG:
self.log.info(" using Series name as Genre") self.log.info(" using Series name as Genre")
# Format the index as a sort key # Format the index as a sort key
@ -2927,7 +2961,7 @@ class ITUNES_ASYNC(ITUNES):
if not oncard: if not oncard:
if DEBUG: if DEBUG:
self.log.info("ITUNES_ASYNC:books()") self.log.info("ITUNES_ASYNC:books()")
if self.settings().use_subdirs: if self.settings().extra_customization[self.CACHE_COVERS]:
self.log.info(" Cover fetching/caching enabled") self.log.info(" Cover fetching/caching enabled")
else: else:
self.log.info(" Cover fetching/caching disabled") self.log.info(" Cover fetching/caching disabled")
@ -3075,6 +3109,38 @@ class ITUNES_ASYNC(ITUNES):
only_presence=False): only_presence=False):
return self.connected, self return self.connected, self
def open(self, library_uuid):
'''
Perform any device specific initialization. Called after the device is
detected but before any other functions that communicate with the device.
For example: For devices that present themselves as USB Mass storage
devices, this method would be responsible for mounting the device or
if the device has been automounted, for finding out where it has been
mounted. The base class within USBMS device.py has a implementation of
this function that should serve as a good example for USB Mass storage
devices.
Note that most of the initialization is necessarily performed in can_handle(), as
we need to talk to iTunes to discover if there's a connected iPod
'''
if DEBUG:
self.log.info("ITUNES_ASYNC.open()")
# Confirm/create thumbs archive
if not os.path.exists(self.cache_dir):
if DEBUG:
self.log.info(" creating thumb cache '%s'" % self.cache_dir)
os.makedirs(self.cache_dir)
if not os.path.exists(self.archive_path):
self.log.info(" creating zip archive")
zfw = ZipFile(self.archive_path, mode='w')
zfw.writestr("iTunes Thumbs Archive",'')
zfw.close()
else:
if DEBUG:
self.log.info(" existing thumb cache at '%s'" % self.archive_path)
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
''' '''
Update metadata on device. Update metadata on device.

View File

@ -282,7 +282,7 @@ def main():
outfile = os.path.join(outfile, path[path.rfind("/")+1:]) outfile = os.path.join(outfile, path[path.rfind("/")+1:])
try: try:
outfile = open(outfile, "wb") outfile = open(outfile, "wb")
except IOError, e: except IOError as e:
print >> sys.stderr, e print >> sys.stderr, e
parser.print_help() parser.print_help()
return 1 return 1
@ -291,13 +291,13 @@ def main():
elif args[1].startswith("prs500:"): elif args[1].startswith("prs500:"):
try: try:
infile = open(args[0], "rb") infile = open(args[0], "rb")
except IOError, e: except IOError as e:
print >> sys.stderr, e print >> sys.stderr, e
parser.print_help() parser.print_help()
return 1 return 1
try: try:
dev.put_file(infile, args[1][7:]) dev.put_file(infile, args[1][7:])
except PathError, err: except PathError as err:
if options.force and 'exists' in str(err): if options.force and 'exists' in str(err):
dev.del_file(err.path, False) dev.del_file(err.path, False)
dev.put_file(infile, args[1][7:]) dev.put_file(infile, args[1][7:])
@ -355,7 +355,7 @@ def main():
return 1 return 1
except DeviceLocked: except DeviceLocked:
print >> sys.stderr, "The device is locked. Use the --unlock option" print >> sys.stderr, "The device is locked. Use the --unlock option"
except (ArgumentError, DeviceError), e: except (ArgumentError, DeviceError) as e:
print >>sys.stderr, e print >>sys.stderr, e
return 1 return 1
return 0 return 0

View File

@ -177,7 +177,7 @@ class PRS500(DeviceConfig, DevicePlugin):
dev.send_validated_command(BeginEndSession(end=True)) dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False dev.in_session = False
raise raise
except USBError, err: except USBError as err:
if "No such device" in str(err): if "No such device" in str(err):
raise DeviceError() raise DeviceError()
elif "Connection timed out" in str(err): elif "Connection timed out" in str(err):
@ -272,7 +272,7 @@ class PRS500(DeviceConfig, DevicePlugin):
self.bulk_read_max_packet_size = red.MaxPacketSize self.bulk_read_max_packet_size = red.MaxPacketSize
self.bulk_write_max_packet_size = wed.MaxPacketSize self.bulk_write_max_packet_size = wed.MaxPacketSize
self.handle.claim_interface(self.INTERFACE_ID) self.handle.claim_interface(self.INTERFACE_ID)
except USBError, err: except USBError as err:
raise DeviceBusy(str(err)) raise DeviceBusy(str(err))
# Large timeout as device may still be initializing # Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000) res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
@ -303,7 +303,7 @@ class PRS500(DeviceConfig, DevicePlugin):
try: try:
self.handle.reset() self.handle.reset()
self.handle.release_interface(self.INTERFACE_ID) self.handle.release_interface(self.INTERFACE_ID)
except Exception, err: except Exception as err:
print >> sys.stderr, err print >> sys.stderr, err
self.handle, self.device = None, None self.handle, self.device = None, None
self.in_session = False self.in_session = False
@ -509,7 +509,7 @@ class PRS500(DeviceConfig, DevicePlugin):
outfile.write("".join(map(chr, packets[0][16:]))) outfile.write("".join(map(chr, packets[0][16:])))
for i in range(1, len(packets)): for i in range(1, len(packets)):
outfile.write("".join(map(chr, packets[i]))) outfile.write("".join(map(chr, packets[i])))
except IOError, err: except IOError as err:
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
raise ArgumentError("File get operation failed. " + \ raise ArgumentError("File get operation failed. " + \
"Could not write to local location: " + str(err)) "Could not write to local location: " + str(err))
@ -656,7 +656,7 @@ class PRS500(DeviceConfig, DevicePlugin):
dest = None dest = None
try: try:
dest = self.path_properties(path, end_session=False) dest = self.path_properties(path, end_session=False)
except PathError, err: except PathError as err:
if "does not exist" in str(err) or "not mounted" in str(err): if "does not exist" in str(err) or "not mounted" in str(err):
return (False, None) return (False, None)
else: raise else: raise

View File

@ -124,11 +124,11 @@ class Device(DeviceConfig, DevicePlugin):
if not prefix: if not prefix:
return 0, 0 return 0, 0
prefix = prefix[:-1] prefix = prefix[:-1]
win32file = __import__('win32file', globals(), locals(), [], -1) import win32file
try: try:
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix) win32file.GetDiskFreeSpace(prefix)
except Exception, err: except Exception as err:
if getattr(err, 'args', [None])[0] == 21: # Disk not ready if getattr(err, 'args', [None])[0] == 21: # Disk not ready
time.sleep(3) time.sleep(3)
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
@ -771,7 +771,7 @@ class Device(DeviceConfig, DevicePlugin):
for d in drives: for d in drives:
try: try:
eject(d) eject(d)
except Exception, e: except Exception as e:
print 'Udisks eject call for:', d, 'failed:' print 'Udisks eject call for:', d, 'failed:'
print '\t', e print '\t', e
failures = True failures = True

View File

@ -57,7 +57,7 @@ class HTMLRenderer(object):
buf.open(QBuffer.WriteOnly) buf.open(QBuffer.WriteOnly)
image.save(buf, 'JPEG') image.save(buf, 'JPEG')
self.data = str(ba.data()) self.data = str(ba.data())
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
finally: finally:

View File

@ -151,7 +151,7 @@ class Container(object):
if name in self.mime_map: if name in self.mime_map:
try: try:
raw = self._parse(raw, self.mime_map[name]) raw = self._parse(raw, self.mime_map[name])
except XMLSyntaxError, err: except XMLSyntaxError as err:
raise ParseError(name, unicode(err)) raise ParseError(name, unicode(err))
self.cache[name] = raw self.cache[name] = raw
return raw return raw

View File

@ -54,7 +54,7 @@ def main(args=sys.argv):
epub = os.path.abspath(args[1]) epub = os.path.abspath(args[1])
try: try:
run(epub, opts, default_log) run(epub, opts, default_log)
except ParseError, err: except ParseError as err:
default_log.error(unicode(err)) default_log.error(unicode(err))
raise SystemExit(1) raise SystemExit(1)

View File

@ -111,6 +111,7 @@ class FB2MLizer(object):
metadata['lang'] = u'en' metadata['lang'] = u'en'
metadata['id'] = None metadata['id'] = None
metadata['cover'] = self.get_cover() metadata['cover'] = self.get_cover()
metadata['genre'] = self.opts.fb2_genre
metadata['author'] = u'' metadata['author'] = u''
for auth in self.oeb_book.metadata.creator: for auth in self.oeb_book.metadata.creator:
@ -159,7 +160,7 @@ class FB2MLizer(object):
return u'<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">' \ return u'<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">' \
'<description>' \ '<description>' \
'<title-info>' \ '<title-info>' \
'<genre>antique</genre>' \ '<genre>%(genre)s</genre>' \
'%(author)s' \ '%(author)s' \
'<book-title>%(title)s</book-title>' \ '<book-title>%(title)s</book-title>' \
'%(cover)s' \ '%(cover)s' \

View File

@ -15,6 +15,133 @@ class FB2Output(OutputFormatPlugin):
author = 'John Schember' author = 'John Schember'
file_type = 'fb2' file_type = 'fb2'
FB2_GENRES = [
# Science Fiction & Fantasy
'sf_history', # Alternative history
'sf_action', # Action
'sf_epic', # Epic
'sf_heroic', # Heroic
'sf_detective', # Detective
'sf_cyberpunk', # Cyberpunk
'sf_space', # Space
'sf_social', # Social#philosophical
'sf_horror', # Horror & mystic
'sf_humor', # Humor
'sf_fantasy', # Fantasy
'sf', # Science Fiction
# Detectives & Thrillers
'det_classic', # Classical detectives
'det_police', # Police Stories
'det_action', # Action
'det_irony', # Ironical detectives
'det_history', # Historical detectives
'det_espionage', # Espionage detectives
'det_crime', # Crime detectives
'det_political', # Political detectives
'det_maniac', # Maniacs
'det_hard', # Hard#boiled
'thriller', # Thrillers
'detective', # Detectives
# Prose
'prose_classic', # Classics prose
'prose_history', # Historical prose
'prose_contemporary', # Contemporary prose
'prose_counter', # Counterculture
'prose_rus_classic', # Russial classics prose
'prose_su_classics', # Soviet classics prose
# Romance
'love_contemporary', # Contemporary Romance
'love_history', # Historical Romance
'love_detective', # Detective Romance
'love_short', # Short Romance
'love_erotica', # Erotica
# Adventure
'adv_western', # Western
'adv_history', # History
'adv_indian', # Indians
'adv_maritime', # Maritime Fiction
'adv_geo', # Travel & geography
'adv_animal', # Nature & animals
'adventure', # Other
# Children's
'child_tale', # Fairy Tales
'child_verse', # Verses
'child_prose', # Prose
'child_sf', # Science Fiction
'child_det', # Detectives & Thrillers
'child_adv', # Adventures
'child_education', # Educational
'children', # Other
# Poetry & Dramaturgy
'poetry', # Poetry
'dramaturgy', # Dramaturgy
# Antique literature
'antique_ant', # Antique
'antique_european', # European
'antique_russian', # Old russian
'antique_east', # Old east
'antique_myths', # Myths. Legends. Epos
'antique', # Other
# Scientific#educational
'sci_history', # History
'sci_psychology', # Psychology
'sci_culture', # Cultural science
'sci_religion', # Religious studies
'sci_philosophy', # Philosophy
'sci_politics', # Politics
'sci_business', # Business literature
'sci_juris', # Jurisprudence
'sci_linguistic', # Linguistics
'sci_medicine', # Medicine
'sci_phys', # Physics
'sci_math', # Mathematics
'sci_chem', # Chemistry
'sci_biology', # Biology
'sci_tech', # Technical
'science', # Other
# Computers & Internet
'comp_www', # Internet
'comp_programming', # Programming
'comp_hard', # Hardware
'comp_soft', # Software
'comp_db', # Databases
'comp_osnet', # OS & Networking
'computers', # Other
# Reference
'ref_encyc', # Encyclopedias
'ref_dict', # Dictionaries
'ref_ref', # Reference
'ref_guide', # Guidebooks
'reference', # Other
# Nonfiction
'nonf_biography', # Biography & Memoirs
'nonf_publicism', # Publicism
'nonf_criticism', # Criticism
'design', # Art & design
'nonfiction', # Other
# Religion & Inspiration
'religion_rel', # Religion
'religion_esoterics', # Esoterics
'religion_self', # Self#improvement
'religion', # Other
# Humor
'humor_anecdote', # Anecdote (funny stories)
'humor_prose', # Prose
'humor_verse', # Verses
'humor', # Other
# Home & Family
'home_cooking', # Cooking
'home_pets', # Pets
'home_crafts', # Hobbies & Crafts
'home_entertain', # Entertaining
'home_health', # Health
'home_garden', # Garden
'home_diy', # Do it yourself
'home_sport', # Sports
'home_sex', # Erotica & sex
'home', # Other
]
options = set([ options = set([
OptionRecommendation(name='sectionize', OptionRecommendation(name='sectionize',
recommended_value='files', level=OptionRecommendation.LOW, recommended_value='files', level=OptionRecommendation.LOW,
@ -25,6 +152,11 @@ class FB2Output(OutputFormatPlugin):
'A value of "Table of Contents" turns the entries in the Table of Contents into titles and creates sections; ' 'A value of "Table of Contents" turns the entries in the Table of Contents into titles and creates sections; '
'if it fails, adjust the "Structure Detection" and/or "Table of Contents" settings ' 'if it fails, adjust the "Structure Detection" and/or "Table of Contents" settings '
'(turn on "Force use of auto-generated Table of Contents).')), '(turn on "Force use of auto-generated Table of Contents).')),
OptionRecommendation(name='fb2_genre',
recommended_value='antique', level=OptionRecommendation.LOW,
choices=FB2_GENRES,
help=_('Genre for the book. Choices: %s\n\n See: ' % FB2_GENRES) + 'http://www.fictionbook.org/index.php/Eng:FictionBook_2.1_genres ' \
+ _('for a complete list with descriptions.')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):

View File

@ -110,7 +110,7 @@ class HTMLFile(object):
try: try:
with open(self.path, 'rb') as f: with open(self.path, 'rb') as f:
src = f.read() src = f.read()
except IOError, err: except IOError as err:
msg = 'Could not read from file: %s with error: %s'%(self.path, as_unicode(err)) msg = 'Could not read from file: %s with error: %s'%(self.path, as_unicode(err))
if level == 0: if level == 0:
raise IOError(msg) raise IOError(msg)
@ -202,7 +202,7 @@ def traverse(path_to_html_file, max_levels=sys.maxint, verbose=0, encoding=None)
raise IgnoreFile('%s is a binary file'%nf.path, -1) raise IgnoreFile('%s is a binary file'%nf.path, -1)
nl.append(nf) nl.append(nf)
flat.append(nf) flat.append(nf)
except IgnoreFile, err: except IgnoreFile as err:
rejects.append(link) rejects.append(link)
if not err.doesnt_exist or verbose > 1: if not err.doesnt_exist or verbose > 1:
print repr(err) print repr(err)

View File

@ -332,7 +332,7 @@ class HTMLConverter(object):
soup = BeautifulSoup(raw, soup = BeautifulSoup(raw,
convertEntities=BeautifulSoup.XHTML_ENTITIES, convertEntities=BeautifulSoup.XHTML_ENTITIES,
markupMassage=nmassage) markupMassage=nmassage)
except ConversionError, err: except ConversionError as err:
if 'Failed to coerce to unicode' in str(err): if 'Failed to coerce to unicode' in str(err):
raw = unicode(raw, 'utf8', 'replace') raw = unicode(raw, 'utf8', 'replace')
soup = BeautifulSoup(raw, soup = BeautifulSoup(raw,
@ -935,7 +935,7 @@ class HTMLConverter(object):
try: try:
im = PILImage.open(path) im = PILImage.open(path)
except IOError, err: except IOError as err:
self.log.warning('Unable to process image: %s\n%s'%( original_path, err)) self.log.warning('Unable to process image: %s\n%s'%( original_path, err))
return return
encoding = detect_encoding(im) encoding = detect_encoding(im)
@ -953,7 +953,7 @@ class HTMLConverter(object):
pt.close() pt.close()
self.scaled_images[path] = pt self.scaled_images[path] = pt
return pt.name return pt.name
except (IOError, SystemError), err: # PIL chokes on interlaced PNG images as well a some GIF images except (IOError, SystemError) as err: # PIL chokes on interlaced PNG images as well a some GIF images
self.log.warning(_('Unable to process image %s. Error: %s')%(path, err)) self.log.warning(_('Unable to process image %s. Error: %s')%(path, err))
if width == None or height == None: if width == None or height == None:
@ -1013,7 +1013,7 @@ class HTMLConverter(object):
if not self.images.has_key(path): if not self.images.has_key(path):
try: try:
self.images[path] = ImageStream(path, encoding=encoding) self.images[path] = ImageStream(path, encoding=encoding)
except LrsError, err: except LrsError as err:
self.log.warning(_('Could not process image: %s\n%s')%( self.log.warning(_('Could not process image: %s\n%s')%(
original_path, err)) original_path, err))
return return
@ -1768,7 +1768,7 @@ class HTMLConverter(object):
tag_css = self.tag_css(tag)[0] # Table should not inherit CSS tag_css = self.tag_css(tag)[0] # Table should not inherit CSS
try: try:
self.process_table(tag, tag_css) self.process_table(tag, tag_css)
except Exception, err: except Exception as err:
self.log.warning(_('An error occurred while processing a table: %s. Ignoring table markup.')%repr(err)) self.log.warning(_('An error occurred while processing a table: %s. Ignoring table markup.')%repr(err))
self.log.exception('') self.log.exception('')
self.log.debug(_('Bad table:\n%s')%unicode(tag)[:300]) self.log.debug(_('Bad table:\n%s')%unicode(tag)[:300])
@ -1858,7 +1858,7 @@ def process_file(path, options, logger):
tf.close() tf.close()
tim.save(tf.name) tim.save(tf.name)
tpath = tf.name tpath = tf.name
except IOError, err: # PIL sometimes fails, for example on interlaced PNG files except IOError as err: # PIL sometimes fails, for example on interlaced PNG files
logger.warn(_('Could not read cover image: %s'), err) logger.warn(_('Could not read cover image: %s'), err)
options.cover = None options.cover = None
else: else:

View File

@ -34,7 +34,7 @@ License: GPL 2 (http://www.gnu.org/copyleft/gpl.html) or BSD
import re, sys, codecs import re, sys, codecs
from logging import getLogger, StreamHandler, Formatter, \ from logging import getLogger, StreamHandler, Formatter, \
DEBUG, INFO, WARN, ERROR, CRITICAL DEBUG, INFO, WARN, CRITICAL
MESSAGE_THRESHOLD = CRITICAL MESSAGE_THRESHOLD = CRITICAL
@ -95,7 +95,7 @@ def removeBOM(text, encoding):
# and uses the actual name of the executable called.) # and uses the actual name of the executable called.)
EXECUTABLE_NAME_FOR_USAGE = "python markdown.py" EXECUTABLE_NAME_FOR_USAGE = "python markdown.py"
# --------------- CONSTANTS YOU _SHOULD NOT_ HAVE TO CHANGE ---------- # --------------- CONSTANTS YOU _SHOULD NOT_ HAVE TO CHANGE ----------
@ -242,8 +242,6 @@ class Element:
if bidi: if bidi:
orig_bidi = self.bidi
if not self.bidi or self.isDocumentElement: if not self.bidi or self.isDocumentElement:
# Once the bidi is set don't change it (except for doc element) # Once the bidi is set don't change it (except for doc element)
self.bidi = bidi self.bidi = bidi
@ -319,7 +317,7 @@ class Element:
childBuffer += "/>" childBuffer += "/>"
buffer += "<" + self.nodeName buffer += "<" + self.nodeName
if self.nodeName in ['p', 'li', 'ul', 'ol', if self.nodeName in ['p', 'li', 'ul', 'ol',
@ -330,10 +328,10 @@ class Element:
bidi = self.bidi bidi = self.bidi
else: else:
bidi = self.doc.bidi bidi = self.doc.bidi
if bidi=="rtl": if bidi=="rtl":
self.setAttribute("dir", "rtl") self.setAttribute("dir", "rtl")
for attr in self.attributes: for attr in self.attributes:
value = self.attribute_values[attr] value = self.attribute_values[attr]
value = self.doc.normalizeEntities(value, value = self.doc.normalizeEntities(value,
@ -358,7 +356,7 @@ class TextNode:
attrRegExp = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123} attrRegExp = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123}
def __init__ (self, text): def __init__ (self, text):
self.value = text self.value = text
def attributeCallback(self, match): def attributeCallback(self, match):
@ -372,7 +370,7 @@ class TextNode:
text = self.value text = self.value
self.parent.setBidi(getBidiType(text)) self.parent.setBidi(getBidiType(text))
if not text.startswith(HTML_PLACEHOLDER_PREFIX): if not text.startswith(HTML_PLACEHOLDER_PREFIX):
if self.parent.nodeName == "p": if self.parent.nodeName == "p":
text = text.replace("\n", "\n ") text = text.replace("\n", "\n ")
@ -413,11 +411,11 @@ There are two types of preprocessors: TextPreprocessor and Preprocessor.
class TextPreprocessor: class TextPreprocessor:
''' '''
TextPreprocessors are run before the text is broken into lines. TextPreprocessors are run before the text is broken into lines.
Each TextPreprocessor implements a "run" method that takes a pointer to a Each TextPreprocessor implements a "run" method that takes a pointer to a
text string of the document, modifies it as necessary and returns text string of the document, modifies it as necessary and returns
either the same pointer or a pointer to a new string. either the same pointer or a pointer to a new string.
TextPreprocessors must extend markdown.TextPreprocessor. TextPreprocessors must extend markdown.TextPreprocessor.
''' '''
@ -431,18 +429,18 @@ class Preprocessor:
Each preprocessor implements a "run" method that takes a pointer to a Each preprocessor implements a "run" method that takes a pointer to a
list of lines of the document, modifies it as necessary and returns list of lines of the document, modifies it as necessary and returns
either the same pointer or a pointer to a new list. either the same pointer or a pointer to a new list.
Preprocessors must extend markdown.Preprocessor. Preprocessors must extend markdown.Preprocessor.
''' '''
def run(self, lines): def run(self, lines):
pass pass
class HtmlBlockPreprocessor(TextPreprocessor): class HtmlBlockPreprocessor(TextPreprocessor):
"""Removes html blocks from the source text and stores it.""" """Removes html blocks from the source text and stores it."""
def _get_left_tag(self, block): def _get_left_tag(self, block):
return block[1:].replace(">", " ", 1).split()[0].lower() return block[1:].replace(">", " ", 1).split()[0].lower()
@ -451,7 +449,7 @@ class HtmlBlockPreprocessor(TextPreprocessor):
return block.rstrip()[-len(left_tag)-2:-1].lower() return block.rstrip()[-len(left_tag)-2:-1].lower()
def _equal_tags(self, left_tag, right_tag): def _equal_tags(self, left_tag, right_tag):
if left_tag == 'div' or left_tag[0] in ['?', '@', '%']: # handle PHP, etc. if left_tag == 'div' or left_tag[0] in ['?', '@', '%']: # handle PHP, etc.
return True return True
if ("/" + left_tag) == right_tag: if ("/" + left_tag) == right_tag:
@ -467,17 +465,17 @@ class HtmlBlockPreprocessor(TextPreprocessor):
def _is_oneliner(self, tag): def _is_oneliner(self, tag):
return (tag in ['hr', 'hr/']) return (tag in ['hr', 'hr/'])
def run(self, text): def run(self, text):
new_blocks = [] new_blocks = []
text = text.split("\n\n") text = text.split("\n\n")
items = [] items = []
left_tag = '' left_tag = ''
right_tag = '' right_tag = ''
in_tag = False # flag in_tag = False # flag
for block in text: for block in text:
if block.startswith("\n"): if block.startswith("\n"):
block = block[1:] block = block[1:]
@ -485,7 +483,7 @@ class HtmlBlockPreprocessor(TextPreprocessor):
if not in_tag: if not in_tag:
if block.startswith("<"): if block.startswith("<"):
left_tag = self._get_left_tag(block) left_tag = self._get_left_tag(block)
right_tag = self._get_right_tag(left_tag, block) right_tag = self._get_right_tag(left_tag, block)
@ -497,13 +495,13 @@ class HtmlBlockPreprocessor(TextPreprocessor):
if self._is_oneliner(left_tag): if self._is_oneliner(left_tag):
new_blocks.append(block.strip()) new_blocks.append(block.strip())
continue continue
if block[1] == "!": if block[1] == "!":
# is a comment block # is a comment block
left_tag = "--" left_tag = "--"
right_tag = self._get_right_tag(left_tag, block) right_tag = self._get_right_tag(left_tag, block)
# keep checking conditions below and maybe just append # keep checking conditions below and maybe just append
if block.rstrip().endswith(">") \ if block.rstrip().endswith(">") \
and self._equal_tags(left_tag, right_tag): and self._equal_tags(left_tag, right_tag):
new_blocks.append( new_blocks.append(
@ -519,9 +517,9 @@ class HtmlBlockPreprocessor(TextPreprocessor):
else: else:
items.append(block.strip()) items.append(block.strip())
right_tag = self._get_right_tag(left_tag, block) right_tag = self._get_right_tag(left_tag, block)
if self._equal_tags(left_tag, right_tag): if self._equal_tags(left_tag, right_tag):
# if find closing tag # if find closing tag
in_tag = False in_tag = False
@ -532,7 +530,7 @@ class HtmlBlockPreprocessor(TextPreprocessor):
if items: if items:
new_blocks.append(self.stash.store('\n\n'.join(items))) new_blocks.append(self.stash.store('\n\n'.join(items)))
new_blocks.append('\n') new_blocks.append('\n')
return "\n\n".join(new_blocks) return "\n\n".join(new_blocks)
HTML_BLOCK_PREPROCESSOR = HtmlBlockPreprocessor() HTML_BLOCK_PREPROCESSOR = HtmlBlockPreprocessor()
@ -605,7 +603,7 @@ LINE_PREPROCESSOR = LinePreprocessor()
class ReferencePreprocessor(Preprocessor): class ReferencePreprocessor(Preprocessor):
''' '''
Removes reference definitions from the text and stores them for later use. Removes reference definitions from the text and stores them for later use.
''' '''
@ -760,7 +758,7 @@ class BacktickPattern (Pattern):
return el return el
class DoubleTagPattern (SimpleTagPattern): class DoubleTagPattern (SimpleTagPattern):
def handleMatch(self, m, doc): def handleMatch(self, m, doc):
tag1, tag2 = self.tag.split(",") tag1, tag2 = self.tag.split(",")
@ -775,7 +773,6 @@ class HtmlPattern (Pattern):
def handleMatch (self, m, doc): def handleMatch (self, m, doc):
rawhtml = m.group(2) rawhtml = m.group(2)
inline = True
place_holder = self.stash.store(rawhtml) place_holder = self.stash.store(rawhtml)
return doc.createTextNode(place_holder) return doc.createTextNode(place_holder)
@ -926,11 +923,11 @@ There are two types of post-processors: Postprocessor and TextPostprocessor
class Postprocessor: class Postprocessor:
''' '''
Postprocessors are run before the dom it converted back into text. Postprocessors are run before the dom it converted back into text.
Each Postprocessor implements a "run" method that takes a pointer to a Each Postprocessor implements a "run" method that takes a pointer to a
NanoDom document, modifies it as necessary and returns a NanoDom NanoDom document, modifies it as necessary and returns a NanoDom
document. document.
Postprocessors must extend markdown.Postprocessor. Postprocessors must extend markdown.Postprocessor.
There are currently no standard post-processors, but the footnote There are currently no standard post-processors, but the footnote
@ -945,10 +942,10 @@ class Postprocessor:
class TextPostprocessor: class TextPostprocessor:
''' '''
TextPostprocessors are run after the dom it converted back into text. TextPostprocessors are run after the dom it converted back into text.
Each TextPostprocessor implements a "run" method that takes a pointer to a Each TextPostprocessor implements a "run" method that takes a pointer to a
text string, modifies it as necessary and returns a text string. text string, modifies it as necessary and returns a text string.
TextPostprocessors must extend markdown.TextPostprocessor. TextPostprocessors must extend markdown.TextPostprocessor.
''' '''
@ -971,7 +968,7 @@ class RawHtmlTextPostprocessor(TextPostprocessor):
html = '' html = ''
else: else:
html = HTML_REMOVED_TEXT html = HTML_REMOVED_TEXT
text = text.replace("<p>%s\n</p>" % (HTML_PLACEHOLDER % i), text = text.replace("<p>%s\n</p>" % (HTML_PLACEHOLDER % i),
html + "\n") html + "\n")
text = text.replace(HTML_PLACEHOLDER % i, html) text = text.replace(HTML_PLACEHOLDER % i, html)
@ -1031,7 +1028,6 @@ class BlockGuru:
remainder of the original list""" remainder of the original list"""
items = [] items = []
item = -1
i = 0 # to keep track of where we are i = 0 # to keep track of where we are
@ -1187,7 +1183,7 @@ class Markdown:
RAWHTMLTEXTPOSTPROCESSOR] RAWHTMLTEXTPOSTPROCESSOR]
self.prePatterns = [] self.prePatterns = []
self.inlinePatterns = [DOUBLE_BACKTICK_PATTERN, self.inlinePatterns = [DOUBLE_BACKTICK_PATTERN,
BACKTICK_PATTERN, BACKTICK_PATTERN,
@ -1241,7 +1237,7 @@ class Markdown:
configs_for_ext = configs[ext] configs_for_ext = configs[ext]
else: else:
configs_for_ext = [] configs_for_ext = []
extension = module.makeExtension(configs_for_ext) extension = module.makeExtension(configs_for_ext)
extension.extendMarkdown(self, globals()) extension.extendMarkdown(self, globals())
@ -1310,7 +1306,7 @@ class Markdown:
else: else:
buffer.append(line) buffer.append(line)
self._processSection(self.top_element, buffer) self._processSection(self.top_element, buffer)
#self._processSection(self.top_element, self.lines) #self._processSection(self.top_element, self.lines)
# Not sure why I put this in but let's leave it for now. # Not sure why I put this in but let's leave it for now.
@ -1426,7 +1422,7 @@ class Markdown:
for item in list: for item in list:
el.appendChild(item) el.appendChild(item)
def _processUList(self, parent_elem, lines, inList): def _processUList(self, parent_elem, lines, inList):
self._processList(parent_elem, lines, inList, self._processList(parent_elem, lines, inList,
@ -1458,7 +1454,7 @@ class Markdown:
i = 0 # a counter to keep track of where we are i = 0 # a counter to keep track of where we are
for line in lines: for line in lines:
loose = 0 loose = 0
if not line.strip(): if not line.strip():
@ -1477,7 +1473,7 @@ class Markdown:
# Check if the next non-blank line is still a part of the list # Check if the next non-blank line is still a part of the list
if ( RE.regExp['ul'].match(next) or if ( RE.regExp['ul'].match(next) or
RE.regExp['ol'].match(next) or RE.regExp['ol'].match(next) or
RE.regExp['tabbed'].match(next) ): RE.regExp['tabbed'].match(next) ):
# get rid of any white space in the line # get rid of any white space in the line
items[item].append(line.strip()) items[item].append(line.strip())
@ -1618,7 +1614,7 @@ class Markdown:
i = 0 i = 0
while i < len(parts): while i < len(parts):
x = parts[i] x = parts[i]
if isinstance(x, (str, unicode)): if isinstance(x, (str, unicode)):
@ -1641,14 +1637,14 @@ class Markdown:
parts[i] = self.doc.createTextNode(x) parts[i] = self.doc.createTextNode(x)
return parts return parts
def _applyPattern(self, line, pattern, patternIndex): def _applyPattern(self, line, pattern, patternIndex):
""" Given a pattern name, this function checks if the line """ Given a pattern name, this function checks if the line
fits the pattern, creates the necessary elements, and returns fits the pattern, creates the necessary elements, and returns
back a list consisting of NanoDom elements and/or strings. back a list consisting of NanoDom elements and/or strings.
@param line: the text to be processed @param line: the text to be processed
@param pattern: the pattern to be checked @param pattern: the pattern to be checked
@ -1676,19 +1672,19 @@ class Markdown:
if not node.nodeName in ["code", "pre"]: if not node.nodeName in ["code", "pre"]:
for child in node.childNodes: for child in node.childNodes:
if isinstance(child, TextNode): if isinstance(child, TextNode):
result = self._handleInline(child.value, patternIndex+1) result = self._handleInline(child.value, patternIndex+1)
if result: if result:
if result == [child]: if result == [child]:
continue continue
result.reverse() result.reverse()
#to make insertion easier #to make insertion easier
position = node.childNodes.index(child) position = node.childNodes.index(child)
node.removeChild(child) node.removeChild(child)
for item in result: for item in result:
@ -1699,7 +1695,7 @@ class Markdown:
self.doc.createTextNode(item)) self.doc.createTextNode(item))
else: else:
node.insertChild(position, item) node.insertChild(position, item)
@ -1798,14 +1794,14 @@ def markdownFromFile(input = None,
def markdown(text, def markdown(text,
extensions = [], extensions = [],
safe_mode = False): safe_mode = False):
message(DEBUG, "in markdown.markdown(), received text:\n%s" % text) message(DEBUG, "in markdown.markdown(), received text:\n%s" % text)
extension_names = [] extension_names = []
extension_configs = {} extension_configs = {}
for ext in extensions: for ext in extensions:
pos = ext.find("(") pos = ext.find("(")
if pos == -1: if pos == -1:
extension_names.append(ext) extension_names.append(ext)
else: else:
@ -1820,7 +1816,7 @@ def markdown(text,
safe_mode = safe_mode) safe_mode = safe_mode)
return md.convert(text) return md.convert(text)
class Extension: class Extension:
@ -1845,26 +1841,11 @@ Python 2.3 or higher required for advanced command line options.
For lower versions of Python use: For lower versions of Python use:
%s INPUT_FILE > OUTPUT_FILE %s INPUT_FILE > OUTPUT_FILE
""" % EXECUTABLE_NAME_FOR_USAGE """ % EXECUTABLE_NAME_FOR_USAGE
def parse_options(): def parse_options():
import optparse
try:
optparse = __import__("optparse")
except:
if len(sys.argv) == 2:
return {'input': sys.argv[1],
'output': None,
'message_threshold': CRITICAL,
'safe': False,
'extensions': [],
'encoding': None }
else:
print OPTPARSE_WARNING
return None
parser = optparse.OptionParser(usage="%prog INPUTFILE [options]") parser = optparse.OptionParser(usage="%prog INPUTFILE [options]")
parser.add_option("-f", "--file", dest="filename", parser.add_option("-f", "--file", dest="filename",
@ -1881,7 +1862,7 @@ def parse_options():
parser.add_option("-s", "--safe", dest="safe", default=False, parser.add_option("-s", "--safe", dest="safe", default=False,
metavar="SAFE_MODE", metavar="SAFE_MODE",
help="same mode ('replace', 'remove' or 'escape' user's HTML tag)") help="same mode ('replace', 'remove' or 'escape' user's HTML tag)")
parser.add_option("--noisy", parser.add_option("--noisy",
action="store_const", const=DEBUG, dest="verbose", action="store_const", const=DEBUG, dest="verbose",
help="print debug messages") help="print debug messages")
@ -1914,14 +1895,14 @@ def main():
if not options: if not options:
sys.exit(0) sys.exit(0)
markdownFromFile(**options) markdownFromFile(**options)
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.exit(main())
""" Run Markdown from the command line. """ """ Run Markdown from the command line. """

View File

@ -108,7 +108,7 @@ def _get_cover_url(br, asin):
q = 'http://amzn.com/'+asin q = 'http://amzn.com/'+asin
try: try:
raw = br.open_novisit(q).read() raw = br.open_novisit(q).read()
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
return None return None
@ -139,7 +139,7 @@ def get_metadata(br, asin, mi):
q = 'http://amzn.com/'+asin q = 'http://amzn.com/'+asin
try: try:
raw = br.open_novisit(q).read() raw = br.open_novisit(q).read()
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
return False return False

View File

@ -33,7 +33,7 @@ class AmazonFr(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='fr') self.isbn, max_results=10, verbose=self.verbose, lang='fr')
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -50,7 +50,7 @@ class AmazonEs(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='es') self.isbn, max_results=10, verbose=self.verbose, lang='es')
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -67,7 +67,7 @@ class AmazonEn(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='en') self.isbn, max_results=10, verbose=self.verbose, lang='en')
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -84,7 +84,7 @@ class AmazonDe(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='de') self.isbn, max_results=10, verbose=self.verbose, lang='de')
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -103,7 +103,7 @@ class Amazon(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='all') self.isbn, max_results=10, verbose=self.verbose, lang='all')
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -193,7 +193,7 @@ class Query(object):
try: try:
raw = browser.open_novisit(self.urldata, timeout=timeout).read() raw = browser.open_novisit(self.urldata, timeout=timeout).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -226,7 +226,7 @@ class Query(object):
try: try:
urldata = self.urldata + '&page=' + str(i) urldata = self.urldata + '&page=' + str(i)
raw = browser.open_novisit(urldata, timeout=timeout).read() raw = browser.open_novisit(urldata, timeout=timeout).read()
except Exception, e: except Exception as e:
continue continue
if '<title>404 - ' in raw: if '<title>404 - ' in raw:
continue continue
@ -413,7 +413,7 @@ class ResultList(list):
def get_individual_metadata(self, browser, linkdata, verbose): def get_individual_metadata(self, browser, linkdata, verbose):
try: try:
raw = browser.open_novisit(linkdata).read() raw = browser.open_novisit(linkdata).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -445,7 +445,7 @@ class ResultList(list):
# self.clean_entry(entry, invalid_id=inv_ids) # self.clean_entry(entry, invalid_id=inv_ids)
title = self.get_title(entry) title = self.get_title(entry)
authors = self.get_authors(entry) authors = self.get_authors(entry)
except Exception, e: except Exception as e:
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e

View File

@ -91,7 +91,7 @@ class OpenLibraryCovers(CoverDownload): # {{{
br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout) br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout)
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
ans.set() ans.set()
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 302: if callable(getattr(e, 'getcode', None)) and e.getcode() == 302:
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
ans.set() ans.set()
@ -106,7 +106,7 @@ class OpenLibraryCovers(CoverDownload): # {{{
try: try:
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read() ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
result_queue.put((True, ans, 'jpg', self.name)) result_queue.put((True, ans, 'jpg', self.name))
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404: if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
result_queue.put((False, _('ISBN: %s not found')%mi.isbn, '', self.name)) result_queue.put((False, _('ISBN: %s not found')%mi.isbn, '', self.name))
else: else:
@ -131,7 +131,7 @@ class AmazonCovers(CoverDownload): # {{{
get_cover_url(mi.isbn, br) get_cover_url(mi.isbn, br)
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
ans.set() ans.set()
except Exception, e: except Exception as e:
self.debug(e) self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.): def get_covers(self, mi, result_queue, abort, timeout=5.):
@ -145,7 +145,7 @@ class AmazonCovers(CoverDownload): # {{{
raise ValueError('No cover found for ISBN: %s'%mi.isbn) raise ValueError('No cover found for ISBN: %s'%mi.isbn)
cover_data = br.open_novisit(url).read() cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name)) result_queue.put((True, cover_data, 'jpg', self.name))
except Exception, e: except Exception as e:
result_queue.put((False, self.exception_to_string(e), result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name)) traceback.format_exc(), self.name))
@ -215,7 +215,7 @@ class DoubanCovers(CoverDownload): # {{{
try: try:
url = self.DOUBAN_ISBN_URL + isbn + "?apikey=" + self.CALIBRE_DOUBAN_API_KEY url = self.DOUBAN_ISBN_URL + isbn + "?apikey=" + self.CALIBRE_DOUBAN_API_KEY
src = br.open(url, timeout=timeout).read() src = br.open(url, timeout=timeout).read()
except Exception, err: except Exception as err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout): if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
err = Exception(_('Douban.com API timed out. Try again later.')) err = Exception(_('Douban.com API timed out. Try again later.'))
raise err raise err
@ -248,7 +248,7 @@ class DoubanCovers(CoverDownload): # {{{
if self.get_cover_url(mi.isbn, br, timeout=timeout) != None: if self.get_cover_url(mi.isbn, br, timeout=timeout) != None:
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
ans.set() ans.set()
except Exception, e: except Exception as e:
self.debug(e) self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.): def get_covers(self, mi, result_queue, abort, timeout=5.):
@ -259,7 +259,7 @@ class DoubanCovers(CoverDownload): # {{{
url = self.get_cover_url(mi.isbn, br, timeout=timeout) url = self.get_cover_url(mi.isbn, br, timeout=timeout)
cover_data = br.open_novisit(url).read() cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name)) result_queue.put((True, cover_data, 'jpg', self.name))
except Exception, e: except Exception as e:
result_queue.put((False, self.exception_to_string(e), result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name)) traceback.format_exc(), self.name))
# }}} # }}}
@ -302,4 +302,16 @@ def test(isbns): # {{{
if __name__ == '__main__': if __name__ == '__main__':
isbns = sys.argv[1:] + ['9781591025412', '9780307272119'] isbns = sys.argv[1:] + ['9781591025412', '9780307272119']
test(isbns) #test(isbns)
from calibre.ebooks.metadata import MetaInformation
oc = OpenLibraryCovers(None)
for isbn in isbns:
mi = MetaInformation('xx', ['yy'])
mi.isbn = isbn
rq = Queue()
oc.get_covers(mi, rq, Event())
result = rq.get_nowait()
if not result[0]:
print 'Failed for ISBN:', isbn
print result

View File

@ -49,7 +49,7 @@ class DoubanBooks(MetadataSource):
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, self.isbn, max_results=10,
verbose=self.verbose) verbose=self.verbose)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -192,7 +192,7 @@ class ResultList(list):
raw = browser.open(id_url).read() raw = browser.open(id_url).read()
feed = etree.fromstring(raw) feed = etree.fromstring(raw)
x = entry(feed)[0] x = entry(feed)[0]
except Exception, e: except Exception as e:
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e
@ -212,7 +212,7 @@ def search(title=None, author=None, publisher=None, isbn=None,
api_key = CALIBRE_DOUBAN_API_KEY api_key = CALIBRE_DOUBAN_API_KEY
while start > 0 and len(entries) <= max_results: while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher, new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, max_results=max_results, start_index=start, api_key=api_key)(br, verbose) isbn=isbn, max_results=max_results, start_index=start, api_key=api_key)(br, verbose)
if not new: if not new:
break break

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from epub files''' '''Read meta information from epub files'''
import os, re, posixpath, shutil import os, re, posixpath
from cStringIO import StringIO from cStringIO import StringIO
from contextlib import closing from contextlib import closing
@ -192,6 +192,13 @@ def get_metadata(stream, extract_cover=True):
def get_quick_metadata(stream): def get_quick_metadata(stream):
return get_metadata(stream, False) return get_metadata(stream, False)
def _write_new_cover(new_cdata, cpath):
from calibre.utils.magick.draw import save_cover_data_to
new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1])
new_cover.close()
save_cover_data_to(new_cdata, new_cover.name)
return new_cover
def set_metadata(stream, mi, apply_null=False, update_timestamp=False): def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
stream.seek(0) stream.seek(0)
reader = OCFZipReader(stream, root=os.getcwdu()) reader = OCFZipReader(stream, root=os.getcwdu())
@ -208,6 +215,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
new_cdata = open(mi.cover, 'rb').read() new_cdata = open(mi.cover, 'rb').read()
except: except:
pass pass
new_cover = cpath = None
if new_cdata and raster_cover: if new_cdata and raster_cover:
try: try:
cpath = posixpath.join(posixpath.dirname(reader.opf_path), cpath = posixpath.join(posixpath.dirname(reader.opf_path),
@ -215,19 +223,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
cover_replacable = not reader.encryption_meta.is_encrypted(cpath) and \ cover_replacable = not reader.encryption_meta.is_encrypted(cpath) and \
os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg') os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg')
if cover_replacable: if cover_replacable:
from calibre.utils.magick.draw import save_cover_data_to, \ new_cover = _write_new_cover(new_cdata, cpath)
identify
new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1])
resize_to = None
if False: # Resize new cover to same size as old cover
shutil.copyfileobj(reader.open(cpath), new_cover)
new_cover.close()
width, height, fmt = identify(new_cover.name)
resize_to = (width, height)
else:
new_cover.close()
save_cover_data_to(new_cdata, new_cover.name,
resize_to=resize_to)
replacements[cpath] = open(new_cover.name, 'rb') replacements[cpath] = open(new_cover.name, 'rb')
except: except:
import traceback import traceback
@ -249,4 +245,11 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
newopf = StringIO(reader.opf.render()) newopf = StringIO(reader.opf.render())
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf, safe_replace(stream, reader.container[OPF.MIMETYPE], newopf,
extra_replacements=replacements) extra_replacements=replacements)
try:
if cpath is not None:
replacements[cpath].close()
os.remove(replacements[cpath].name)
except:
pass

View File

@ -93,7 +93,7 @@ class MetadataSource(Plugin): # {{{
traceback.print_exc() traceback.print_exc()
mi.comments = None mi.comments = None
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -186,7 +186,7 @@ class GoogleBooks(MetadataSource): # {{{
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, self.isbn, max_results=10,
verbose=self.verbose) verbose=self.verbose)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -217,7 +217,7 @@ class ISBNDB(MetadataSource): # {{{
try: try:
opts, args = option_parser().parse_args(args) opts, args = option_parser().parse_args(args)
self.results = create_books(opts, args) self.results = create_books(opts, args)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -244,7 +244,7 @@ class Amazon(MetadataSource): # {{{
try: try:
self.results = get_social_metadata(self.title, self.book_author, self.results = get_social_metadata(self.title, self.book_author,
self.publisher, self.isbn) self.publisher, self.isbn)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -264,7 +264,7 @@ class KentDistrictLibrary(MetadataSource): # {{{
from calibre.ebooks.metadata.kdl import get_series from calibre.ebooks.metadata.kdl import get_series
try: try:
self.results = get_series(self.title, self.book_author) self.results = get_series(self.title, self.book_author)
except Exception, e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
self.exception = e self.exception = e

View File

@ -30,7 +30,7 @@ class Fictionwise(MetadataSource): # {{{
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose) self.isbn, max_results=10, verbose=self.verbose)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -91,7 +91,7 @@ class Query(object):
try: try:
raw = browser.open_novisit(self.BASE_URL, self.urldata, timeout=timeout).read() raw = browser.open_novisit(self.BASE_URL, self.urldata, timeout=timeout).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -276,7 +276,7 @@ class ResultList(list):
def get_individual_metadata(self, browser, linkdata, verbose): def get_individual_metadata(self, browser, linkdata, verbose):
try: try:
raw = browser.open_novisit(self.BASE_URL + linkdata).read() raw = browser.open_novisit(self.BASE_URL + linkdata).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -311,7 +311,7 @@ class ResultList(list):
#maybe strenghten the search #maybe strenghten the search
ratings = self.get_rating(entry.xpath("./p/table")[1], verbose) ratings = self.get_rating(entry.xpath("./p/table")[1], verbose)
authors = self.get_authors(entry) authors = self.get_authors(entry)
except Exception, e: except Exception as e:
if verbose: if verbose:
print _('Failed to get all details for an entry') print _('Failed to get all details for an entry')
print e print e
@ -328,7 +328,7 @@ class ResultList(list):
#maybe strenghten the search #maybe strenghten the search
ratings = self.get_rating(entry.xpath("./p/table")[1], verbose) ratings = self.get_rating(entry.xpath("./p/table")[1], verbose)
authors = self.get_authors(entry) authors = self.get_authors(entry)
except Exception, e: except Exception as e:
if verbose: if verbose:
print _('Failed to get all details for an entry') print _('Failed to get all details for an entry')
print e print e

View File

@ -176,7 +176,7 @@ class ResultList(list):
raw = browser.open(id_url).read() raw = browser.open(id_url).read()
feed = etree.fromstring(raw) feed = etree.fromstring(raw)
x = entry(feed)[0] x = entry(feed)[0]
except Exception, e: except Exception as e:
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e

View File

@ -38,7 +38,7 @@ def get_metadata(stream):
mi.author = author mi.author = author
if category: if category:
mi.category = category mi.category = category
except Exception, err: except Exception as err:
msg = u'Couldn\'t read metadata from imp: %s with error %s'%(mi.title, unicode(err)) msg = u'Couldn\'t read metadata from imp: %s with error %s'%(mi.title, unicode(err))
print >>sys.stderr, msg.encode('utf8') print >>sys.stderr, msg.encode('utf8')
return mi return mi

View File

@ -25,7 +25,7 @@ def fetch_metadata(url, max=3, timeout=5.):
while len(books) < total_results and max > 0: while len(books) < total_results and max > 0:
try: try:
raw = br.open(url, timeout=timeout).read() raw = br.open(url, timeout=timeout).read()
except Exception, err: except Exception as err:
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err)) raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
soup = BeautifulStoneSoup(raw, soup = BeautifulStoneSoup(raw,
convertEntities=BeautifulStoneSoup.XML_ENTITIES) convertEntities=BeautifulStoneSoup.XML_ENTITIES)

View File

@ -43,7 +43,7 @@ def get_series(title, authors, timeout=60):
br = browser() br = browser()
try: try:
raw = br.open_novisit(url, timeout=timeout).read() raw = br.open_novisit(url, timeout=timeout).read()
except URLError, e: except URLError as e:
if isinstance(e.reason, socket.timeout): if isinstance(e.reason, socket.timeout):
raise Exception('KDL Server busy, try again later') raise Exception('KDL Server busy, try again later')
raise raise

View File

@ -4,34 +4,23 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Fetch cover from LibraryThing.com based on ISBN number. Fetch cover from LibraryThing.com based on ISBN number.
''' '''
import sys, re, random import sys, re
from lxml import html from lxml import html
import mechanize import mechanize
from calibre import browser, prints from calibre import browser, prints, random_user_agent
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.ebooks.chardet import strip_encoding_declarations from calibre.ebooks.chardet import strip_encoding_declarations
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
def get_ua():
choices = [
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)'
'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16'
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19'
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
]
return choices[random.randint(0, len(choices)-1)]
_lt_br = None _lt_br = None
def get_browser(): def get_browser():
global _lt_br global _lt_br
if _lt_br is None: if _lt_br is None:
_lt_br = browser(user_agent=get_ua()) _lt_br = browser(user_agent=random_user_agent())
return _lt_br.clone_browser() return _lt_br.clone_browser()
class HeadRequest(mechanize.Request): class HeadRequest(mechanize.Request):
@ -45,7 +34,7 @@ def check_for_cover(isbn, timeout=5.):
try: try:
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout) br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
return True return True
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 302: if callable(getattr(e, 'getcode', None)) and e.getcode() == 302:
return True return True
return False return False

View File

@ -32,7 +32,7 @@ class NiceBooks(MetadataSource):
try: try:
self.results = search(self.title, self.book_author, self.publisher, self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose) self.isbn, max_results=10, verbose=self.verbose)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -54,7 +54,7 @@ class NiceBooksCovers(CoverDownload):
if Covers(mi.isbn)(entry).check_cover(): if Covers(mi.isbn)(entry).check_cover():
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
ans.set() ans.set()
except Exception, e: except Exception as e:
self.debug(e) self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.): def get_covers(self, mi, result_queue, abort, timeout=5.):
@ -67,7 +67,7 @@ class NiceBooksCovers(CoverDownload):
if not ext: if not ext:
ext = 'jpg' ext = 'jpg'
result_queue.put((True, cover_data, ext, self.name)) result_queue.put((True, cover_data, ext, self.name))
except Exception, e: except Exception as e:
result_queue.put((False, self.exception_to_string(e), result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name)) traceback.format_exc(), self.name))
@ -109,7 +109,7 @@ class Query(object):
try: try:
raw = browser.open_novisit(self.BASE_URL+self.urldata, timeout=timeout).read() raw = browser.open_novisit(self.BASE_URL+self.urldata, timeout=timeout).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -144,7 +144,7 @@ class Query(object):
try: try:
urldata = self.urldata + '&p=' + str(i) urldata = self.urldata + '&p=' + str(i)
raw = browser.open_novisit(self.BASE_URL+urldata, timeout=timeout).read() raw = browser.open_novisit(self.BASE_URL+urldata, timeout=timeout).read()
except Exception, e: except Exception as e:
continue continue
if '<title>404 - ' in raw: if '<title>404 - ' in raw:
continue continue
@ -233,7 +233,7 @@ class ResultList(list):
def get_individual_metadata(self, browser, linkdata, verbose): def get_individual_metadata(self, browser, linkdata, verbose):
try: try:
raw = browser.open_novisit(self.BASE_URL + linkdata).read() raw = browser.open_novisit(self.BASE_URL + linkdata).read()
except Exception, e: except Exception as e:
report(verbose) report(verbose)
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
@ -266,7 +266,7 @@ class ResultList(list):
entry = entry.find("div[@id='book-info']") entry = entry.find("div[@id='book-info']")
title = self.get_title(entry) title = self.get_title(entry)
authors = self.get_authors(entry) authors = self.get_authors(entry)
except Exception, e: except Exception as e:
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e
@ -280,7 +280,7 @@ class ResultList(list):
entry = entry.find("div[@id='book-info']") entry = entry.find("div[@id='book-info']")
title = self.get_title(entry) title = self.get_title(entry)
authors = self.get_authors(entry) authors = self.get_authors(entry)
except Exception, e: except Exception as e:
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e
@ -315,7 +315,7 @@ class Covers(object):
cover, ext = browser.open_novisit(self.urlimg, timeout=timeout).read(), \ cover, ext = browser.open_novisit(self.urlimg, timeout=timeout).read(), \
self.urlimg.rpartition('.')[-1] self.urlimg.rpartition('.')[-1]
return cover, ext if ext else 'jpg' return cover, ext if ext else 'jpg'
except Exception, err: except Exception as err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout): if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
raise NiceBooksError(_('Nicebooks timed out. Try again later.')) raise NiceBooksError(_('Nicebooks timed out. Try again later.'))
if not len(self.urlimg): if not len(self.urlimg):

View File

@ -43,7 +43,7 @@ def get_metadata(stream):
elif key.strip() == 'AUTHOR': elif key.strip() == 'AUTHOR':
mi.author = value mi.author = value
mi.authors = string_to_authors(value) mi.authors = string_to_authors(value)
except Exception, err: except Exception as err:
msg = u'Couldn\'t read metadata from rb: %s with error %s'%(mi.title, unicode(err)) msg = u'Couldn\'t read metadata from rb: %s with error %s'%(mi.title, unicode(err))
print >>sys.stderr, msg.encode('utf8') print >>sys.stderr, msg.encode('utf8')
raise raise

View File

@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en'
import socket, time, re import socket, time, re
from urllib import urlencode from urllib import urlencode
from threading import Thread from threading import Thread
from Queue import Queue, Empty
from lxml.html import soupparser, tostring from lxml.html import soupparser, tostring
@ -41,12 +42,12 @@ class Worker(Thread): # {{{
try: try:
self.get_details() self.get_details()
except: except:
self.log.error('get_details failed for url: %r'%self.url) self.log.exception('get_details failed for url: %r'%self.url)
def get_details(self): def get_details(self):
try: try:
raw = self.browser.open_novisit(self.url, timeout=self.timeout).read().strip() raw = self.browser.open_novisit(self.url, timeout=self.timeout).read().strip()
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
self.log.error('URL malformed: %r'%self.url) self.log.error('URL malformed: %r'%self.url)
@ -168,9 +169,11 @@ class Worker(Thread): # {{{
if self.isbn: if self.isbn:
self.plugin.cache_isbn_to_identifier(self.isbn, self.amazon_id) self.plugin.cache_isbn_to_identifier(self.isbn, self.amazon_id)
if self.cover_url: if self.cover_url:
self.cache_identifier_to_cover_url(self.amazon_id, self.plugin.cache_identifier_to_cover_url(self.amazon_id,
self.cover_url) self.cover_url)
self.plugin.clean_downloaded_metadata(mi)
self.result_queue.put(mi) self.result_queue.put(mi)
def parse_asin(self, root): def parse_asin(self, root):
@ -276,7 +279,7 @@ class Amazon(Source):
name = 'Amazon' name = 'Amazon'
description = _('Downloads metadata from Amazon') description = _('Downloads metadata from Amazon')
capabilities = frozenset(['identify']) capabilities = frozenset(['identify', 'cover'])
touched_fields = frozenset(['title', 'authors', 'identifier:amazon', touched_fields = frozenset(['title', 'authors', 'identifier:amazon',
'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate']) 'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate'])
@ -284,6 +287,7 @@ class Amazon(Source):
'com': _('US'), 'com': _('US'),
'fr' : _('France'), 'fr' : _('France'),
'de' : _('Germany'), 'de' : _('Germany'),
'uk' : _('UK'),
} }
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
@ -331,7 +335,7 @@ class Amazon(Source):
# }}} # }}}
def get_cached_cover_url(self, identifiers): def get_cached_cover_url(self, identifiers): # {{{
url = None url = None
asin = identifiers.get('amazon', None) asin = identifiers.get('amazon', None)
if asin is None: if asin is None:
@ -344,6 +348,7 @@ class Amazon(Source):
url = self.cached_identifier_to_cover_url(asin) url = self.cached_identifier_to_cover_url(asin)
return url return url
# }}}
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
identifiers={}, timeout=30): identifiers={}, timeout=30):
@ -359,7 +364,7 @@ class Amazon(Source):
br = self.browser br = self.browser
try: try:
raw = br.open_novisit(query, timeout=timeout).read().strip() raw = br.open_novisit(query, timeout=timeout).read().strip()
except Exception, e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and \ if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404: e.getcode() == 404:
log.error('Query malformed: %r'%query) log.error('Query malformed: %r'%query)
@ -442,8 +447,44 @@ class Amazon(Source):
return None 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 __name__ == '__main__': if abort.is_set():
return
br = self.browser
try:
cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put(cdata)
except:
log.exception('Failed to download cover from:', cached_url)
# }}}
if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e # To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/amazon.py # src/calibre/ebooks/metadata/sources/amazon.py
from calibre.ebooks.metadata.sources.test import (test_identify_plugin, from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
@ -489,5 +530,5 @@ if __name__ == '__main__':
), ),
]) ])
# }}}

View File

@ -8,11 +8,13 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re, threading import re, threading
from future_builtins import map
from calibre import browser, random_user_agent from calibre import browser, random_user_agent
from calibre.customize import Plugin from calibre.customize import Plugin
from calibre.utils.logging import ThreadSafeLog, FileStream from calibre.utils.logging import ThreadSafeLog, FileStream
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
from calibre.utils.titlecase import titlecase
msprefs = JSONConfig('metadata_sources.json') msprefs = JSONConfig('metadata_sources.json')
@ -21,6 +23,7 @@ def create_log(ostream=None):
log.outputs = [FileStream(ostream)] log.outputs = [FileStream(ostream)]
return log return log
# Comparing Metadata objects for relevance {{{
words = ("the", "a", "an", "of", "and") words = ("the", "a", "an", "of", "and")
prefix_pat = re.compile(r'^(%s)\s+'%("|".join(words))) prefix_pat = re.compile(r'^(%s)\s+'%("|".join(words)))
trailing_paren_pat = re.compile(r'\(.*\)$') trailing_paren_pat = re.compile(r'\(.*\)$')
@ -35,6 +38,55 @@ def cleanup_title(s):
s = whitespace_pat.sub(' ', s) s = whitespace_pat.sub(' ', s)
return s.strip() return s.strip()
class InternalMetadataCompareKeyGen(object):
'''
Generate a sort key for comparison of the relevance of Metadata objects,
given a search query.
The sort key ensures that an ascending order sort is a sort by order of
decreasing relevance.
The algorithm is:
* Prefer results that have the same ISBN as specified in the query
* Prefer results with a cached cover URL
* Prefer results with all available fields filled in
* Prefer results that are an exact title match to the query
* Prefer results with longer comments (greater than 10% longer)
* Use the relevance of the result as reported by the metadata source's search
engine
'''
def __init__(self, mi, source_plugin, title, authors, identifiers):
isbn = 1 if mi.isbn and mi.isbn == identifiers.get('isbn', None) else 2
all_fields = 1 if source_plugin.test_fields(mi) is None else 2
exact_title = 1 if title and \
cleanup_title(title) == cleanup_title(mi.title) else 2
has_cover = 2 if source_plugin.get_cached_cover_url(mi.identifiers)\
is None else 1
self.base = (isbn, has_cover, all_fields, exact_title)
self.comments_len = len(mi.comments.strip() if mi.comments else '')
self.extra = (getattr(mi, 'source_relevance', 0), )
def __cmp__(self, other):
result = cmp(self.base, other.base)
if result == 0:
# Now prefer results with the longer comments, within 10%
cx, cy = self.comments_len, other.comments_len
t = (cx + cy) / 20
delta = cy - cx
if abs(delta) > t:
result = delta
else:
result = cmp(self.extra, other.extra)
return result
# }}}
class Source(Plugin): class Source(Plugin):
@ -43,8 +95,12 @@ class Source(Plugin):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
#: Set of capabilities supported by this plugin.
#: Useful capabilities are: 'identify', 'cover'
capabilities = frozenset() capabilities = frozenset()
#: List of metadata fields that can potentially be download by this plugin
#: during the identify phase
touched_fields = frozenset() touched_fields = frozenset()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -70,11 +126,17 @@ class Source(Plugin):
def browser(self): def browser(self):
if self._browser is None: if self._browser is None:
self._browser = browser(user_agent=random_user_agent()) self._browser = browser(user_agent=random_user_agent())
return self._browser return self._browser.clone_browser()
# }}} # }}}
# Utility functions {{{ # Caching {{{
def get_related_isbns(self, id_):
with self.cache_lock:
for isbn, q in self._isbn_to_identifier_cache.iteritems():
if q == id_:
yield isbn
def cache_isbn_to_identifier(self, isbn, identifier): def cache_isbn_to_identifier(self, isbn, identifier):
with self.cache_lock: with self.cache_lock:
@ -92,6 +154,10 @@ class Source(Plugin):
with self.cache_lock: with self.cache_lock:
return self._identifier_to_cover_url_cache.get(id_, None) return self._identifier_to_cover_url_cache.get(id_, None)
# }}}
# Utility functions {{{
def get_author_tokens(self, authors, only_first_author=True): def get_author_tokens(self, authors, only_first_author=True):
''' '''
Take a list of authors and return a list of tokens useful for an Take a list of authors and return a list of tokens useful for an
@ -156,6 +222,20 @@ class Source(Plugin):
elif mi.is_null(key): elif mi.is_null(key):
return key return key
def clean_downloaded_metadata(self, mi):
'''
Call this method in your plugin's identify method to normalize metadata
before putting the Metadata object into result_queue. You can of
course, use a custom algorithm suited to your metadata source.
'''
def fixcase(x):
if x:
x = titlecase(x)
return x
if mi.title:
mi.title = fixcase(mi.title)
mi.authors = list(map(fixcase, mi.authors))
mi.tags = list(map(fixcase, mi.tags))
# }}} # }}}
@ -164,72 +244,33 @@ class Source(Plugin):
def get_cached_cover_url(self, identifiers): def get_cached_cover_url(self, identifiers):
''' '''
Return cached cover URL for the book identified by Return cached cover URL for the book identified by
the identifiers dict or Noneif no such URL exists the identifiers dict or None if no such URL exists.
Note that this method must only return validated URLs, i.e. not URLS
that could result in a generic cover image or a not found error.
''' '''
return None return None
def compare_identify_results(self, x, y, title=None, authors=None, def identify_results_keygen(self, title=None, authors=None,
identifiers={}): identifiers={}):
''' '''
Method used to sort the results from a call to identify by relevance. Return a function that is used to generate a key that can sort Metadata
Uses the actual query and various heuristics to rank results. objects by their relevance given a search query (title, authors,
Re-implement in your plugin if this generic algorithm is not suitable. identifiers).
Note that this method assumes x and y have a source_relevance
attribute.
one < two iff one is more relevant than two These keys are used to sort the results of a call to :meth:`identify`.
For details on the default algorithm see
:class:`InternalMetadataCompareKeyGen`. Re-implement this function in
your plugin if the default algorithm is not suitable.
''' '''
# First, guarantee that if the query specifies an ISBN, the result with def keygen(mi):
# the same isbn is the most relevant return InternalMetadataCompareKeyGen(mi, self, title, authors,
def isbn_test(mi): identifiers)
return mi.isbn and mi.isbn == identifiers.get('isbn', None) return keygen
def boolcmp(a, b):
return -1 if a and not b else 1 if not a and b else 0
x_has_isbn, y_has_isbn = isbn_test(x), isbn_test(y)
result = boolcmp(x_has_isbn, y_has_isbn)
if result != 0:
return result
# Now prefer results that have complete metadata over those that don't
x_has_all_fields = self.test_fields(x) is None
y_has_all_fields = self.test_fields(y) is None
result = boolcmp(x_has_all_fields, y_has_all_fields)
if result != 0:
return result
# Now prefer results whose title matches the search query
if title:
x_title = cleanup_title(x.title)
y_title = cleanup_title(y.title)
t = cleanup_title(title)
x_has_title, y_has_title = x_title == t, y_title == t
result = boolcmp(x_has_title, y_has_title)
if result != 0:
return result
# Now prefer results with the longer comments, within 10%
cx = len(x.comments.strip() if x.comments else '')
cy = len(y.comments.strip() if y.comments else '')
t = (cx + cy) / 20
result = cy - cx
if result != 0 and abs(cx - cy) > t:
return result
# Now prefer results with cached cover URLs
x_has_cover = self.get_cached_cover_url(x.identifiers) is not None
y_has_cover = self.get_cached_cover_url(y.identifiers) is not None
result = boolcmp(x_has_cover, y_has_cover)
if result != 0:
return result
# Now use the relevance reported by the remote search engine
return x.source_relevance - y.source_relevance
def identify(self, log, result_queue, abort, title=None, authors=None, def identify(self, log, result_queue, abort, title=None, authors=None,
identifiers={}, timeout=5): identifiers={}, timeout=30):
''' '''
Identify a book by its title/author/isbn/etc. Identify a book by its title/author/isbn/etc.
@ -269,5 +310,17 @@ class Source(Plugin):
''' '''
return None return None
def download_cover(self, log, result_queue, abort,
title=None, authors=None, identifiers={}, timeout=30):
'''
Download a cover and put it into result_queue. The parameters all have
the same meaning as for :meth:`identify`.
This method should use cached cover URLs for efficiency whenever
possible. When cached data is not present, most plugins simply call
identify and use its results.
'''
pass
# }}} # }}}

View File

@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en'
import time import time
from urllib import urlencode from urllib import urlencode
from functools import partial from functools import partial
from Queue import Queue, Empty
from lxml import etree from lxml import etree
@ -24,7 +25,8 @@ from calibre import as_unicode
NAMESPACES = { NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/', 'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom', 'atom' : 'http://www.w3.org/2005/Atom',
'dc': 'http://purl.org/dc/terms' 'dc' : 'http://purl.org/dc/terms',
'gd' : 'http://schemas.google.com/g/2005'
} }
XPath = partial(etree.XPath, namespaces=NAMESPACES) XPath = partial(etree.XPath, namespaces=NAMESPACES)
@ -41,6 +43,7 @@ publisher = XPath('descendant::dc:publisher')
subject = XPath('descendant::dc:subject') subject = XPath('descendant::dc:subject')
description = XPath('descendant::dc:description') description = XPath('descendant::dc:description')
language = XPath('descendant::dc:language') language = XPath('descendant::dc:language')
rating = XPath('descendant::gd:rating[@average]')
def get_details(browser, url, timeout): # {{{ def get_details(browser, url, timeout): # {{{
try: try:
@ -113,8 +116,10 @@ def to_metadata(browser, log, entry_, timeout): # {{{
btags = [x.text for x in subject(extra) if x.text] btags = [x.text for x in subject(extra) if x.text]
tags = [] tags = []
for t in btags: for t in btags:
tags.extend([y.strip() for y in t.split('/')]) atags = [y.strip() for y in t.split('/')]
tags = list(sorted(list(set(tags)))) for tag in atags:
if tag not in tags:
tags.append(tag)
except: except:
log.exception('Failed to parse tags:') log.exception('Failed to parse tags:')
tags = [] tags = []
@ -130,6 +135,18 @@ def to_metadata(browser, log, entry_, timeout): # {{{
except: except:
log.exception('Failed to parse pubdate') log.exception('Failed to parse pubdate')
# Ratings
for x in rating(extra):
try:
mi.rating = float(x.get('average'))
if mi.rating > 5:
mi.rating /= 2
except:
log.exception('Failed to parse rating')
# Cover
mi.has_google_cover = len(extra.xpath(
'//*[@rel="http://schemas.google.com/books/2008/thumbnail"]')) > 0
return mi return mi
# }}} # }}}
@ -139,11 +156,13 @@ class GoogleBooks(Source):
name = 'Google Books' name = 'Google Books'
description = _('Downloads metadata from Google Books') description = _('Downloads metadata from Google Books')
capabilities = frozenset(['identify']) capabilities = frozenset(['identify', 'cover'])
touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate', touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate',
'comments', 'publisher', 'identifier:isbn', 'comments', 'publisher', 'identifier:isbn', 'rating',
'identifier:google']) # language currently disabled 'identifier:google']) # language currently disabled
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1'
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
BASE_URL = 'http://books.google.com/books/feeds/volumes?' BASE_URL = 'http://books.google.com/books/feeds/volumes?'
isbn = check_isbn(identifiers.get('isbn', None)) isbn = check_isbn(identifiers.get('isbn', None))
@ -174,30 +193,70 @@ class GoogleBooks(Source):
}) })
# }}} # }}}
def cover_url_from_identifiers(self, identifiers): 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.cover_url_from_identifiers(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
br = self.browser
try:
cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put(cdata)
except:
log.exception('Failed to download cover from:', cached_url)
# }}}
def get_cached_cover_url(self, identifiers): # {{{
url = None
goog = identifiers.get('google', None) goog = identifiers.get('google', None)
if goog is None: if goog is None:
isbn = identifiers.get('isbn', None) isbn = identifiers.get('isbn', None)
goog = self.cached_isbn_to_identifier(isbn) if isbn is not None:
goog = self.cached_isbn_to_identifier(isbn)
if goog is not None: if goog is not None:
return ('http://books.google.com/books?id=%s&printsec=frontcover&img=1' % url = self.cached_identifier_to_cover_url(goog)
goog)
def is_cover_image_valid(self, raw): return url
# When no cover is present, returns a PNG saying image not available # }}}
# Try for example google identifier llNqPwAACAAJ
# I have yet to see an actual cover in PNG format
return raw and len(raw) > 17000 and raw[1:4] != 'PNG'
def get_all_details(self, br, log, entries, abort, result_queue, timeout): def get_all_details(self, br, log, entries, abort, # {{{
result_queue, timeout):
for relevance, i in enumerate(entries): for relevance, i in enumerate(entries):
try: try:
ans = to_metadata(br, log, i, timeout) ans = to_metadata(br, log, i, timeout)
if isinstance(ans, Metadata): if isinstance(ans, Metadata):
ans.source_relevance = relevance ans.source_relevance = relevance
goog = ans.identifiers['google']
for isbn in getattr(ans, 'all_isbns', []): for isbn in getattr(ans, 'all_isbns', []):
self.cache_isbn_to_identifier(isbn, self.cache_isbn_to_identifier(isbn, goog)
ans.identifiers['google']) if ans.has_google_cover:
self.cache_identifier_to_cover_url(goog,
self.GOOGLE_COVER%goog)
self.clean_downloaded_metadata(ans)
result_queue.put(ans) result_queue.put(ans)
except: except:
log.exception( log.exception(
@ -205,6 +264,7 @@ class GoogleBooks(Source):
etree.tostring(i)) etree.tostring(i))
if abort.is_set(): if abort.is_set():
break break
# }}}
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
identifiers={}, timeout=30): identifiers={}, timeout=30):
@ -213,7 +273,7 @@ class GoogleBooks(Source):
br = self.browser br = self.browser
try: try:
raw = br.open_novisit(query, timeout=timeout).read() raw = br.open_novisit(query, timeout=timeout).read()
except Exception, e: except Exception as e:
log.exception('Failed to make identify query: %r'%query) log.exception('Failed to make identify query: %r'%query)
return as_unicode(e) return as_unicode(e)
@ -222,7 +282,7 @@ class GoogleBooks(Source):
feed = etree.fromstring(xml_to_unicode(clean_ascii_chars(raw), feed = etree.fromstring(xml_to_unicode(clean_ascii_chars(raw),
strip_encoding_pats=True)[0], parser=parser) strip_encoding_pats=True)[0], parser=parser)
entries = entry(feed) entries = entry(feed)
except Exception, e: except Exception as e:
log.exception('Failed to parse identify results') log.exception('Failed to parse identify results')
return as_unicode(e) return as_unicode(e)
@ -238,7 +298,7 @@ class GoogleBooks(Source):
return None return None
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e src/calibre/ebooks/metadata/sources/google.py # To run these test use: calibre-debug -e src/calibre/ebooks/metadata/sources/google.py
from calibre.ebooks.metadata.sources.test import (test_identify_plugin, from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
title_test, authors_test) title_test, authors_test)
@ -253,8 +313,10 @@ if __name__ == '__main__':
authors_test(['Francis Scott Fitzgerald'])] authors_test(['Francis Scott Fitzgerald'])]
), ),
#( (
# {'title': 'Great Expectations', 'authors':['Charles Dickens']}, {'title': 'Flatland', 'authors':['Abbott']},
# [title_test('Great Expectations', exact=True)] [title_test('Flatland', exact=False)]
#), ),
]) ])
# }}}

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time
from Queue import Queue, Empty
from threading import Thread
from io import BytesIO
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata.sources.base import create_log
# How long to wait for more results after first result is found
WAIT_AFTER_FIRST_RESULT = 30 # seconds
class Worker(Thread):
def __init__(self, plugin, kwargs, abort):
Thread.__init__(self)
self.daemon = True
self.plugin, self.kwargs, self.rq = plugin, kwargs, Queue()
self.abort = abort
self.buf = BytesIO()
self.log = create_log(self.buf)
def run(self):
try:
self.plugin.identify(self.log, self.rq, self.abort, **self.kwargs)
except:
self.log.exception('Plugin', self.plugin.name, 'failed')
def is_worker_alive(workers):
for w in workers:
if w.is_alive():
return True
return False
def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
plugins = list(metadata_plugins['identify'])
kwargs = {
'title': title,
'authors': authors,
'identifiers': identifiers,
'timeout': timeout,
}
log('Running identify query with parameters:')
log(kwargs)
log('Using plugins:', ', '.join([p.name for p in plugins]))
log('The log (if any) from individual plugins is below')
workers = [Worker(p, kwargs, abort) for p in plugins]
for w in workers:
w.start()
first_result_at = None
results = dict.fromkeys(plugins, [])
def get_results():
found = False
for w in workers:
try:
result = w.rq.get_nowait()
except Empty:
pass
else:
results[w.plugin].append(result)
found = True
return found
while True:
time.sleep(0.2)
if get_results() and first_result_at is None:
first_result_at = time.time()
if not is_worker_alive(workers):
break
if (first_result_at is not None and time.time() - first_result_at <
WAIT_AFTER_FIRST_RESULT):
log('Not waiting any longer for more results')
abort.set()
break
get_results()
sort_kwargs = dict(kwargs)
for k in list(sort_kwargs.iterkeys()):
if k not in ('title', 'authors', 'identifiers'):
sort_kwargs.pop(k)
for plugin, results in results.iteritems():
results.sort(key=plugin.identify_results_keygen(**sort_kwargs))
plog = plugin.buf.getvalue().strip()
if plog:
log('\n'+'*'*35, plugin.name, '*'*35)
log('Found %d results'%len(results))
log(plog)
log('\n'+'*'*80)

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.ebooks.metadata.sources.base import Source
class OpenLibrary(Source):
name = 'Open Library'
description = _('Downloads metadata from The Open Library')
capabilities = frozenset(['cover'])
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
def download_cover(self, log, result_queue, abort,
title=None, authors=None, identifiers={}, timeout=30):
if 'isbn' not in identifiers:
return
isbn = identifiers['isbn']
br = self.browser
try:
ans = br.open_novisit(self.OPENLIBRARY%isbn, timeout=timeout).read()
result_queue.put(ans)
except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
log.error('No cover for ISBN: %r found'%isbn)
else:
log.exception('Failed to download cover for ISBN:', isbn)

View File

@ -11,9 +11,8 @@ import os, tempfile, time
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Event from threading import Event
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
from calibre import prints from calibre import prints, sanitize_file_name2
from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.sources.base import create_log from calibre.ebooks.metadata.sources.base import create_log
@ -90,11 +89,19 @@ def test_identify_plugin(name, tests):
except Empty: except Empty:
break break
prints('Found', len(results), 'matches:') prints('Found', len(results), 'matches:', end=' ')
prints('Smaller relevance means better match')
for mi in results: results.sort(key=plugin.identify_results_keygen(
title=kwargs.get('title', None), authors=kwargs.get('authors',
None), identifiers=kwargs.get('identifiers', {})))
for i, mi in enumerate(results):
prints('*'*30, 'Relevance:', i, '*'*30)
prints(mi) prints(mi)
prints('\n\n') prints('\nCached cover URL :',
plugin.get_cached_cover_url(mi.identifiers))
prints('*'*75, '\n\n')
possibles = [] possibles = []
for mi in results: for mi in results:
@ -117,6 +124,36 @@ def test_identify_plugin(name, tests):
prints('Failed to find', plugin.test_fields(possibles[0])) prints('Failed to find', plugin.test_fields(possibles[0]))
raise SystemExit(1) raise SystemExit(1)
if results[0] is not possibles[0]:
prints('Most relevant result failed the tests')
raise SystemExit(1)
if 'cover' in plugin.capabilities:
rq = Queue()
mi = results[0]
plugin.download_cover(log, rq, abort, title=mi.title,
authors=mi.authors, identifiers=mi.identifiers)
results = []
while True:
try:
results.append(rq.get_nowait())
except Empty:
break
if not results:
prints('Cover download failed')
raise SystemExit(1)
cdata = results[0]
cover = os.path.join(tdir, plugin.name.replace(' ',
'')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ',
'_')))
with open(cover, 'wb') as f:
f.write(cdata)
prints('Cover downloaded to:', cover)
if len(cdata) < 10240:
prints('Downloaded cover too small')
raise SystemExit(1)
prints('Average time per query', sum(times)/len(times)) prints('Average time per query', sum(times)/len(times))

View File

@ -147,7 +147,7 @@ class TOC(list):
if path and os.access(path, os.R_OK): if path and os.access(path, os.R_OK):
try: try:
self.read_ncx_toc(path) self.read_ncx_toc(path)
except Exception, err: except Exception as err:
print 'WARNING: Invalid NCX file:', err print 'WARNING: Invalid NCX file:', err
return return
cwd = os.path.abspath(self.base_path) cwd = os.path.abspath(self.base_path)

View File

@ -769,7 +769,8 @@ class MobiReader(object):
def extract_text(self): def extract_text(self):
self.log.debug('Extracting text...') self.log.debug('Extracting text...')
text_sections = [self.text_section(i) for i in range(1, self.book_header.records + 1)] text_sections = [self.text_section(i) for i in range(1,
min(self.book_header.records + 1, len(self.sections)))]
processed_records = list(range(0, self.book_header.records + 1)) processed_records = list(range(0, self.book_header.records + 1))
self.mobi_html = '' self.mobi_html = ''

View File

@ -884,13 +884,13 @@ class Manifest(object):
def first_pass(data): def first_pass(data):
try: try:
data = etree.fromstring(data, parser=parser) data = etree.fromstring(data, parser=parser)
except etree.XMLSyntaxError, err: except etree.XMLSyntaxError as err:
self.oeb.log.exception('Initial parse failed:') self.oeb.log.exception('Initial parse failed:')
repl = lambda m: ENTITYDEFS.get(m.group(1), m.group(0)) repl = lambda m: ENTITYDEFS.get(m.group(1), m.group(0))
data = ENTITY_RE.sub(repl, data) data = ENTITY_RE.sub(repl, data)
try: try:
data = etree.fromstring(data, parser=parser) data = etree.fromstring(data, parser=parser)
except etree.XMLSyntaxError, err: except etree.XMLSyntaxError as err:
self.oeb.logger.warn('Parsing file %r as HTML' % self.href) self.oeb.logger.warn('Parsing file %r as HTML' % self.href)
if err.args and err.args[0].startswith('Excessive depth'): if err.args and err.args[0].startswith('Excessive depth'):
from lxml.html import soupparser from lxml.html import soupparser

View File

@ -10,10 +10,10 @@ import re
from lxml import etree from lxml import etree
from urlparse import urlparse from urlparse import urlparse
from collections import OrderedDict
from calibre.ebooks.oeb.base import XPNSMAP, TOC, XHTML, xml2text from calibre.ebooks.oeb.base import XPNSMAP, TOC, XHTML, xml2text
from calibre.ebooks import ConversionError from calibre.ebooks import ConversionError
from calibre.utils.ordered_dict import OrderedDict
def XPath(x): def XPath(x):
try: try:

View File

@ -103,7 +103,7 @@ def main(args=sys.argv, name=''):
try: try:
decrypt(args[0], opts.output, args[1]) decrypt(args[0], opts.output, args[1])
except DecryptionError, e: except DecryptionError as e:
print e.value print e.value
return 1 return 1

View File

@ -50,7 +50,7 @@ def pdftohtml(output_dir, pdf_path, no_images):
try: try:
p = popen(cmd, stderr=logf._fd, stdout=logf._fd, p = popen(cmd, stderr=logf._fd, stdout=logf._fd,
stdin=subprocess.PIPE) stdin=subprocess.PIPE)
except OSError, err: except OSError as err:
if err.errno == 2: if err.errno == 2:
raise ConversionError(_('Could not find pdftohtml, check it is in your PATH')) raise ConversionError(_('Could not find pdftohtml, check it is in your PATH'))
else: else:
@ -60,7 +60,7 @@ def pdftohtml(output_dir, pdf_path, no_images):
try: try:
ret = p.wait() ret = p.wait()
break break
except OSError, e: except OSError as e:
if e.errno == errno.EINTR: if e.errno == errno.EINTR:
continue continue
else: else:

View File

@ -268,7 +268,7 @@ class RTFInput(InputFormatPlugin):
self.log('Converting RTF to XML...') self.log('Converting RTF to XML...')
try: try:
xml = self.generate_xml(stream.name) xml = self.generate_xml(stream.name)
except RtfInvalidCodeException, e: except RtfInvalidCodeException as e:
raise ValueError(_('This RTF file has a feature calibre does not ' raise ValueError(_('This RTF file has a feature calibre does not '
'support. Convert it to HTML first and then try it.\n%s')%e) 'support. Convert it to HTML first and then try it.\n%s')%e)

View File

@ -245,8 +245,11 @@ class Colors:
self.__token_info = line[:16] self.__token_info = line[:16]
action = self.__state_dict.get(self.__state) action = self.__state_dict.get(self.__state)
if action is None: if action is None:
sys.stderr.write('no matching state in module fonts.py\n') try:
sys.stderr.write(self.__state + '\n') sys.stderr.write('no matching state in module fonts.py\n')
sys.stderr.write(self.__state + '\n')
except:
pass
action(line) action(line)
copy_obj = copy.Copy(bug_handler = self.__bug_handler) copy_obj = copy.Copy(bug_handler = self.__bug_handler)
if self.__copy: if self.__copy:

View File

@ -85,7 +85,7 @@ class SNBFile:
uncompressedData += bzdc.decompress(data) uncompressedData += bzdc.decompress(data)
else: else:
uncompressedData += data uncompressedData += data
except Exception, e: except Exception as e:
print e print e
if len(uncompressedData) != self.plainStreamSizeUncompressed: if len(uncompressedData) != self.plainStreamSizeUncompressed:
raise Exception() raise Exception()

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
PyTextile PyTextile
@ -62,6 +64,8 @@ import re
import uuid import uuid
from urlparse import urlparse from urlparse import urlparse
from calibre.utils.smartypants import smartyPants
def _normalize_newlines(string): def _normalize_newlines(string):
out = re.sub(r'\r\n', '\n', string) out = re.sub(r'\r\n', '\n', string)
out = re.sub(r'\n{3,}', '\n\n', out) out = re.sub(r'\n{3,}', '\n\n', out)
@ -206,6 +210,12 @@ class Textile(object):
(re.compile(r'{clubs?}'), r'&#9827;'), # club (re.compile(r'{clubs?}'), r'&#9827;'), # club
(re.compile(r'{hearts?}'), r'&#9829;'), # heart (re.compile(r'{hearts?}'), r'&#9829;'), # heart
(re.compile(r'{diam(onds?|s)}'), r'&#9830;'), # diamond (re.compile(r'{diam(onds?|s)}'), r'&#9830;'), # diamond
(re.compile(r'{"}'), r'&#34;'), # double-quote
(re.compile(r"{'}"), r'&#39;'), # single-quote
(re.compile(r"{(|'/|/')}"), r'&#8217;'), # closing-single-quote - apostrophe
(re.compile(r"{(|\\'|'\\)}"), r'&#8216;'), # opening-single-quote
(re.compile(r'{(”|"/|/")}'), r'&#8221;'), # closing-double-quote
(re.compile(r'{(“|\\"|"\\)}'), r'&#8220;'), # opening-double-quote
] ]
glyph_defaults = [ glyph_defaults = [
(re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), r'\1\2&#215;\3'), # dimension sign (re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), r'\1\2&#215;\3'), # dimension sign
@ -254,10 +264,9 @@ class Textile(object):
self.rel = ' rel="%s"' % rel self.rel = ' rel="%s"' % rel
text = self.getRefs(text) text = self.getRefs(text)
text = self.block(text, int(head_offset)) text = self.block(text, int(head_offset))
text = self.retrieve(text) text = self.retrieve(text)
text = smartyPants(text, 'q')
return text return text

View File

@ -165,7 +165,6 @@ class TXTInput(InputFormatPlugin):
elif options.formatting_type == 'textile': elif options.formatting_type == 'textile':
log.debug('Running text through textile conversion...') log.debug('Running text through textile conversion...')
html = convert_textile(txt) html = convert_textile(txt)
setattr(options, 'smarten_punctuation', True)
else: else:
log.debug('Running text through basic conversion...') log.debug('Running text through basic conversion...')
flow_size = getattr(options, 'flow_size', 0) flow_size = getattr(options, 'flow_size', 0)

View File

@ -32,7 +32,7 @@ class Worker(Thread):
def run(self): def run(self):
try: try:
self.doit() self.doit()
except Exception, err: except Exception as err:
import traceback import traceback
try: try:
err = unicode(err) err = unicode(err)

View File

@ -78,7 +78,7 @@ class RecursiveFind(QThread): # {{{
if isinstance(root, unicode): if isinstance(root, unicode):
root = root.encode(filesystem_encoding) root = root.encode(filesystem_encoding)
self.walk(root) self.walk(root)
except Exception, err: except Exception as err:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
try: try:

View File

@ -46,6 +46,13 @@ class PluginWidget(QWidget, Ui_Form):
for x in xrange(self.db_fields.count()): for x in xrange(self.db_fields.count()):
item = self.db_fields.item(x) item = self.db_fields.item(x)
item.setSelected(unicode(item.text()) in fields) item.setSelected(unicode(item.text()) in fields)
self.bibfile_enc.clear()
self.bibfile_enc.addItems(['utf-8', 'cp1252', 'ascii/LaTeX'])
self.bibfile_enctag.clear()
self.bibfile_enctag.addItems(['strict', 'replace', 'ignore',
'backslashreplace'])
self.bib_entry.clear()
self.bib_entry.addItems(['mixed', 'misc', 'book'])
# Update dialog fields from stored options # Update dialog fields from stored options
for opt in self.OPTION_FIELDS: for opt in self.OPTION_FIELDS:
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1]) opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])

View File

@ -29,23 +29,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QComboBox" name="bibfile_enc"> <widget class="QComboBox" name="bibfile_enc"/>
<item>
<property name="text">
<string notr="true">utf-8</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">cp1252</string>
</property>
</item>
<item>
<property name="text">
<string>ascii/LaTeX</string>
</property>
</item>
</widget>
</item> </item>
<item row="1" column="1" rowspan="11"> <item row="1" column="1" rowspan="11">
<widget class="QListWidget" name="db_fields"> <widget class="QListWidget" name="db_fields">
@ -71,28 +55,7 @@
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QComboBox" name="bibfile_enctag"> <widget class="QComboBox" name="bibfile_enctag"/>
<item>
<property name="text">
<string>strict</string>
</property>
</item>
<item>
<property name="text">
<string>replace</string>
</property>
</item>
<item>
<property name="text">
<string>ignore</string>
</property>
</item>
<item>
<property name="text">
<string>backslashreplace</string>
</property>
</item>
</widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
@ -115,23 +78,7 @@
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="6" column="0">
<widget class="QComboBox" name="bib_entry"> <widget class="QComboBox" name="bib_entry"/>
<item>
<property name="text">
<string>mixed</string>
</property>
</item>
<item>
<property name="text">
<string>misc</string>
</property>
</item>
<item>
<property name="text">
<string>book</string>
</property>
</item>
</widget>
</item> </item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QCheckBox" name="impcit"> <widget class="QCheckBox" name="impcit">

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import textwrap, codecs import textwrap, codecs, importlib
from functools import partial from functools import partial
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
@ -22,8 +22,8 @@ from calibre.customize.ui import plugin_for_input_format
def config_widget_for_input_plugin(plugin): def config_widget_for_input_plugin(plugin):
name = plugin.name.lower().replace(' ', '_') name = plugin.name.lower().replace(' ', '_')
try: try:
return __import__('calibre.gui2.convert.'+name, return importlib.import_module(
fromlist=[1]).PluginWidget 'calibre.gui2.convert.'+name).PluginWidget
except ImportError: except ImportError:
pass pass

View File

@ -4,7 +4,7 @@ __license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>' __copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import shutil import shutil, importlib
from PyQt4.Qt import QString, SIGNAL from PyQt4.Qt import QString, SIGNAL
@ -82,8 +82,8 @@ class BulkConfig(Config):
output_widget = None output_widget = None
name = self.plumber.output_plugin.name.lower().replace(' ', '_') name = self.plumber.output_plugin.name.lower().replace(' ', '_')
try: try:
output_widget = __import__('calibre.gui2.convert.'+name, output_widget = importlib.import_module(
fromlist=[1]) 'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget pw = output_widget.PluginWidget
pw.ICON = I('back.png') pw.ICON = I('back.png')
pw.HELP = _('Options specific to the output format.') pw.HELP = _('Options specific to the output format.')

View File

@ -17,8 +17,10 @@ class PluginWidget(Widget, Ui_Form):
ICON = I('mimetypes/fb2.png') ICON = I('mimetypes/fb2.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['sectionize']) Widget.__init__(self, parent, ['sectionize', 'fb2_genre'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
for x in ('toc', 'files', 'nothing'): for x in ('toc', 'files', 'nothing'):
self.opt_sectionize.addItem(x) self.opt_sectionize.addItem(x)
for x in get_option('fb2_genre').option.choices:
self.opt_fb2_genre.addItem(x)
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -14,7 +14,7 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0"> <item row="2" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -29,21 +29,31 @@
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Sectionize:</string> <string>Sectionize:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>opt_sectionize</cstring> <cstring>opt_sectionize</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_sectionize">
<property name="minimumContentsLength">
<number>20</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_sectionize">
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Genre</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_fb2_genre"/>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -192,7 +192,7 @@ class MetadataWidget(Widget, Ui_Form):
try: try:
cf = open(_file, "rb") cf = open(_file, "rb")
cover = cf.read() cover = cf.read()
except IOError, e: except IOError as e:
d = error_dialog(self.parent(), _('Error reading file'), d = error_dialog(self.parent(), _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e)) _("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_() d.exec_()

View File

@ -69,7 +69,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
try: try:
pat = unicode(x.regex) pat = unicode(x.regex)
re.compile(pat) re.compile(pat)
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid regular expression'), error_dialog(self, _('Invalid regular expression'),
_('Invalid regular expression: %s')%err, show=True) _('Invalid regular expression: %s')%err, show=True)
return False return False

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, cPickle, shutil import sys, cPickle, shutil, importlib
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
@ -182,8 +182,8 @@ class Config(ResizableDialog, Ui_Dialog):
output_widget = None output_widget = None
name = self.plumber.output_plugin.name.lower().replace(' ', '_') name = self.plumber.output_plugin.name.lower().replace(' ', '_')
try: try:
output_widget = __import__('calibre.gui2.convert.'+name, output_widget = importlib.import_module(
fromlist=[1]) 'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget pw = output_widget.PluginWidget
pw.ICON = I('back.png') pw.ICON = I('back.png')
pw.HELP = _('Options specific to the output format.') pw.HELP = _('Options specific to the output format.')
@ -193,8 +193,8 @@ class Config(ResizableDialog, Ui_Dialog):
input_widget = None input_widget = None
name = self.plumber.input_plugin.name.lower().replace(' ', '_') name = self.plumber.input_plugin.name.lower().replace(' ', '_')
try: try:
input_widget = __import__('calibre.gui2.convert.'+name, input_widget = importlib.import_module(
fromlist=[1]) 'calibre.gui2.convert.'+name)
pw = input_widget.PluginWidget pw = input_widget.PluginWidget
pw.ICON = I('forward.png') pw.ICON = I('forward.png')
pw.HELP = _('Options specific to the input format.') pw.HELP = _('Options specific to the input format.')

View File

@ -64,7 +64,7 @@ class DeviceJob(BaseJob): # {{{
self.result = self.func(*self.args, **self.kwargs) self.result = self.func(*self.args, **self.kwargs)
if self._aborted: if self._aborted:
return return
except (Exception, SystemExit), err: except (Exception, SystemExit) as err:
if self._aborted: if self._aborted:
return return
self.failed = True self.failed = True
@ -162,7 +162,7 @@ class DeviceManager(Thread): # {{{
dev.reset(detected_device=detected_device, dev.reset(detected_device=detected_device,
report_progress=self.report_progress) report_progress=self.report_progress)
dev.open(self.current_library_uuid) dev.open(self.current_library_uuid)
except OpenFeedback, e: except OpenFeedback as e:
if dev not in self.ejected_devices: if dev not in self.ejected_devices:
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg) self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
self.ejected_devices.add(dev) self.ejected_devices.add(dev)

View File

@ -133,7 +133,7 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
try: try:
validation_formatter.validate(tmpl) validation_formatter.validate(tmpl)
return True return True
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid template'), error_dialog(self, _('Invalid template'),
'<p>'+_('The template %s is invalid:')%tmpl + \ '<p>'+_('The template %s is invalid:')%tmpl + \
'<br>'+unicode(err), show=True) '<br>'+unicode(err), show=True)

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, sys import os, sys, importlib
from calibre.customize.ui import config from calibre.customize.ui import config
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
@ -43,8 +43,7 @@ class Catalog(ResizableDialog, Ui_Dialog):
name = plugin.name.lower().replace(' ', '_') name = plugin.name.lower().replace(' ', '_')
if type(plugin) in builtin_plugins: if type(plugin) in builtin_plugins:
try: try:
catalog_widget = __import__('calibre.gui2.catalog.'+name, catalog_widget = importlib.import_module('calibre.gui2.catalog.'+name)
fromlist=[1])
pw = catalog_widget.PluginWidget() pw = catalog_widget.PluginWidget()
pw.initialize(name, db) pw.initialize(name, db)
pw.ICON = I('forward.png') pw.ICON = I('forward.png')
@ -75,7 +74,7 @@ class Catalog(ResizableDialog, Ui_Dialog):
# Import the dynamic PluginWidget() from .py file provided in plugin.zip # Import the dynamic PluginWidget() from .py file provided in plugin.zip
try: try:
sys.path.insert(0, plugin.resources_path) sys.path.insert(0, plugin.resources_path)
catalog_widget = __import__(name, fromlist=[1]) catalog_widget = importlib.import_module(name)
pw = catalog_widget.PluginWidget() pw = catalog_widget.PluginWidget()
pw.initialize(name) pw.initialize(name)
pw.ICON = I('forward.png') pw.ICON = I('forward.png')

View File

@ -68,7 +68,7 @@ class DBCheck(QDialog): # {{{
self.start_load() self.start_load()
return return
QTimer.singleShot(0, self.do_one_dump) QTimer.singleShot(0, self.do_one_dump)
except Exception, e: except Exception as e:
import traceback import traceback
self.error = (as_unicode(e), traceback.format_exc()) self.error = (as_unicode(e), traceback.format_exc())
self.reject() self.reject()
@ -90,7 +90,7 @@ class DBCheck(QDialog): # {{{
self.conn.commit() self.conn.commit()
QTimer.singleShot(0, self.do_one_load) QTimer.singleShot(0, self.do_one_load)
except Exception, e: except Exception as e:
import traceback import traceback
self.error = (as_unicode(e), traceback.format_exc()) self.error = (as_unicode(e), traceback.format_exc())
self.reject() self.reject()
@ -111,7 +111,7 @@ class DBCheck(QDialog): # {{{
self.pb.setValue(self.pb.value() + 1) self.pb.setValue(self.pb.value() + 1)
self.count -= 1 self.count -= 1
QTimer.singleShot(0, self.do_one_load) QTimer.singleShot(0, self.do_one_load)
except Exception, e: except Exception as e:
import traceback import traceback
self.error = (as_unicode(e), traceback.format_exc()) self.error = (as_unicode(e), traceback.format_exc())
self.reject() self.reject()

View File

@ -120,7 +120,7 @@ class MyBlockingBusy(QDialog): # {{{
self.msg.setText(self.msg_text.format(self.phases[self.current_phase], self.msg.setText(self.msg_text.format(self.phases[self.current_phase],
percent)) percent))
self.do_one(id) self.do_one(id)
except Exception, err: except Exception as err:
import traceback import traceback
try: try:
err = unicode(err) err = unicode(err)

View File

@ -76,7 +76,7 @@ class CoverFetcher(Thread): # {{{
self.cover_data, self.errors = download_cover(mi, self.cover_data, self.errors = download_cover(mi,
timeout=self.timeout) timeout=self.timeout)
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
print self.traceback print self.traceback
@ -183,7 +183,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
try: try:
cf = open(_file, "rb") cf = open(_file, "rb")
cover = cf.read() cover = cf.read()
except IOError, e: except IOError as e:
d = error_dialog(self, _('Error reading file'), d = error_dialog(self, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e)) _("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_() d.exec_()

View File

@ -9,6 +9,7 @@ Scheduler for automated recipe downloads
from datetime import timedelta from datetime import timedelta
import calendar, textwrap import calendar, textwrap
from collections import OrderedDict
from PyQt4.Qt import QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout, \ from PyQt4.Qt import QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout, \
QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout, \ QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout, \
@ -20,7 +21,6 @@ from calibre.web.feeds.recipes.model import RecipeModel
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import utcnow from calibre.utils.date import utcnow
from calibre.utils.network import internet_connected from calibre.utils.network import internet_connected
from calibre.utils.ordered_dict import OrderedDict
from calibre import force_unicode from calibre import force_unicode
def convert_day_time_schedule(val): def convert_day_time_schedule(val):

View File

@ -122,6 +122,8 @@ class TagEditor(QDialog, Ui_TagEditor):
tags = unicode(self.add_tag_input.text()).split(',') tags = unicode(self.add_tag_input.text()).split(',')
for tag in tags: for tag in tags:
tag = tag.strip() tag = tag.strip()
if not tag:
continue
for item in self.available_tags.findItems(tag, Qt.MatchFixedString): for item in self.available_tags.findItems(tag, Qt.MatchFixedString):
self.available_tags.takeItem(self.available_tags.row(item)) self.available_tags.takeItem(self.available_tags.row(item))
if tag not in self.tags: if tag not in self.tags:

View File

@ -237,7 +237,7 @@ class %(classname)s(%(base_class)s):
try: try:
compile_recipe(src) compile_recipe(src)
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid input'), error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_() _('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return return
@ -246,7 +246,7 @@ class %(classname)s(%(base_class)s):
src = unicode(self.source_code.toPlainText()) src = unicode(self.source_code.toPlainText())
try: try:
title = compile_recipe(src).title title = compile_recipe(src).title
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid input'), error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_() _('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return return
@ -333,7 +333,7 @@ class %(classname)s(%(base_class)s):
try: try:
profile = open(file, 'rb').read().decode('utf-8') profile = open(file, 'rb').read().decode('utf-8')
title = compile_recipe(profile).title title = compile_recipe(profile).title
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid input'), error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_() _('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return return

View File

@ -35,7 +35,7 @@ class Worker(Thread): # {{{
try: try:
br = browser() br = browser()
br.retrieve(self.url, self.fpath, self.callback) br.retrieve(self.url, self.fpath, self.callback)
except Exception, e: except Exception as e:
self.err = as_unicode(e) self.err = as_unicode(e)
import traceback import traceback
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@ -143,21 +143,27 @@ def dnd_has_extension(md, extensions):
urls = [unicode(u.toString()) for u in urls = [unicode(u.toString()) for u in
md.urls()] md.urls()]
purls = [urlparse(u) for u in urls] purls = [urlparse(u) for u in urls]
paths = [u2p(x) for x in purls]
if DEBUG: if DEBUG:
prints('URLS:', urls) prints('URLS:', urls)
prints('Paths:', [u2p(x) for x in purls]) prints('Paths:', paths)
exts = frozenset([posixpath.splitext(u.path)[1][1:].lower() for u in exts = frozenset([posixpath.splitext(u)[1][1:].lower() for u in
purls]) paths])
return bool(exts.intersection(frozenset(extensions))) return bool(exts.intersection(frozenset(extensions)))
return False return False
def _u2p(raw):
path = raw
if iswindows and path.startswith('/'):
path = path[1:]
return path.replace('/', os.sep)
def u2p(url): def u2p(url):
path = url.path path = url.path
if iswindows: ans = _u2p(path)
if path.startswith('/'): if not os.path.exists(ans):
path = path[1:] ans = _u2p(url.path + '#' + url.fragment)
ans = path.replace('/', os.sep)
if os.path.exists(ans): if os.path.exists(ans):
return ans return ans
# Try unquoting the URL # Try unquoting the URL
@ -189,8 +195,9 @@ def dnd_get_image(md, image_exts=IMAGE_EXTENSIONS):
md.urls()] md.urls()]
purls = [urlparse(u) for u in urls] purls = [urlparse(u) for u in urls]
# First look for a local file # First look for a local file
images = [u2p(x) for x in purls if x.scheme in ('', 'file') and images = [u2p(x) for x in purls if x.scheme in ('', 'file')]
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in images = [x for x in images if
posixpath.splitext(urllib.unquote(x))[1][1:].lower() in
image_exts] image_exts]
images = [x for x in images if os.path.exists(x)] images = [x for x in images if os.path.exists(x)]
p = QPixmap() p = QPixmap()
@ -235,8 +242,9 @@ def dnd_get_files(md, exts):
md.urls()] md.urls()]
purls = [urlparse(u) for u in urls] purls = [urlparse(u) for u in urls]
# First look for a local file # First look for a local file
local_files = [u2p(x) for x in purls if x.scheme in ('', 'file') and local_files = [u2p(x) for x in purls if x.scheme in ('', 'file')]
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in local_files = [ p for p in local_files if
posixpath.splitext(urllib.unquote(p))[1][1:].lower() in
exts] exts]
local_files = [x for x in local_files if os.path.exists(x)] local_files = [x for x in local_files if os.path.exists(x)]
if local_files: if local_files:

View File

@ -116,7 +116,7 @@ class Emailer(Thread): # {{{
try: try:
self.sendmail(job) self.sendmail(job)
break break
except Exception, e: except Exception as e:
if not self._run: if not self._run:
return return
import traceback import traceback

View File

@ -398,7 +398,7 @@ class CcTemplateDelegate(QStyledItemDelegate): # {{{
val = unicode(editor.textbox.toPlainText()) val = unicode(editor.textbox.toPlainText())
try: try:
validation_formatter.validate(val) validation_formatter.validate(val)
except Exception, err: except Exception as err:
error_dialog(self.parent(), _('Invalid template'), error_dialog(self.parent(), _('Invalid template'),
'<p>'+_('The template %s is invalid:')%val + \ '<p>'+_('The template %s is invalid:')%val + \
'<br>'+str(err), show=True) '<br>'+str(err), show=True)

View File

@ -35,7 +35,7 @@ class RenderWorker(QThread):
self.stream = None self.stream = None
if self.aborted: if self.aborted:
self.lrf = None self.lrf = None
except Exception, err: except Exception as err:
self.lrf, self.stream = None, None self.lrf, self.stream = None, None
self.exception = err self.exception = err
self.formatted_traceback = traceback.format_exc() self.formatted_traceback = traceback.format_exc()

View File

@ -399,7 +399,7 @@ def main(args=sys.argv):
if __name__ == '__main__': if __name__ == '__main__':
try: try:
sys.exit(main()) sys.exit(main())
except Exception, err: except Exception as err:
if not iswindows: raise if not iswindows: raise
tb = traceback.format_exc() tb = traceback.format_exc()
from PyQt4.QtGui import QErrorMessage from PyQt4.QtGui import QErrorMessage

View File

@ -656,7 +656,7 @@ class Cover(ImageView): # {{{
try: try:
cf = open(_file, "rb") cf = open(_file, "rb")
cover = cf.read() cover = cf.read()
except IOError, e: except IOError as e:
d = error_dialog(self, _('Error reading file'), d = error_dialog(self, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") _("<p>There was an error reading from file: <br /><b>")
+ _file + "</b></p><br />"+str(e)) + _file + "</b></p><br />"+str(e))

View File

@ -88,7 +88,7 @@ class DownloadMetadata(Thread):
def run(self): def run(self):
try: try:
self._run() self._run()
except Exception, e: except Exception as e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()

View File

@ -303,7 +303,7 @@ class MetadataSingleDialogBase(ResizableDialog):
return False return False
self.books_to_refresh |= getattr(widget, 'books_to_refresh', self.books_to_refresh |= getattr(widget, 'books_to_refresh',
set([])) set([]))
except IOError, err: except IOError as err:
if err.errno == 13: # Permission denied if err.errno == 13: # Permission denied
import traceback import traceback
fname = err.filename if err.filename else 'file' fname = err.filename if err.filename else 'file'

View File

@ -34,7 +34,7 @@ class DBUSNotifier(Notifier):
import dbus import dbus
self.dbus = dbus self.dbus = dbus
self._notify = dbus.Interface(dbus.SessionBus().get_object(server, path), interface) self._notify = dbus.Interface(dbus.SessionBus().get_object(server, path), interface)
except Exception, err: except Exception as err:
self.ok = False self.ok = False
self.err = str(err) self.err = str(err)

View File

@ -31,9 +31,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
db = gui.library_view.model().db db = gui.library_view.model().db
r = self.register r = self.register
choices = [(_('Low'), 'low'), (_('Normal'), 'normal'), (_('High'),
r('worker_process_priority', prefs, choices= 'high')] if iswindows else \
[(_('Low'), 'low'), (_('Normal'), 'normal'), (_('High'), 'high')]) [(_('Normal'), 'normal'), (_('Low'), 'low'), (_('Very low'),
'high')]
r('worker_process_priority', prefs, choices=choices)
r('network_timeout', prefs) r('network_timeout', prefs)
@ -60,9 +62,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal) signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
signal.connect(self.internally_viewed_formats_changed) signal.connect(self.internally_viewed_formats_changed)
self.settings['worker_process_priority'].gui_obj.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import importlib
from PyQt4.Qt import QIcon, Qt, QStringListModel, QVariant from PyQt4.Qt import QIcon, Qt, QStringListModel, QVariant
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
@ -104,8 +106,8 @@ class OutputOptions(Base):
for plugin in output_format_plugins(): for plugin in output_format_plugins():
name = plugin.name.lower().replace(' ', '_') name = plugin.name.lower().replace(' ', '_')
try: try:
output_widget = __import__('calibre.gui2.convert.'+name, output_widget = importlib.import_module(
fromlist=[1]) 'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget pw = output_widget.PluginWidget
self.conversion_widgets.append(pw) self.conversion_widgets.append(pw)
except ImportError: except ImportError:

View File

@ -148,8 +148,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'rating': _('My Rating'), 'rating': _('My Rating'),
'last_modified':_('Modified Date'), 'last_modified':_('Modified Date'),
'people': _('People')}[which]) 'people': _('People')}[which])
if which == 'people': self.is_names.setChecked(which == 'people')
self.is_names.setChecked(True)
if self.composite_box.isVisible(): if self.composite_box.isVisible():
self.composite_box.setText( self.composite_box.setText(
{ {

View File

@ -9,7 +9,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>603</width> <width>831</width>
<height>344</height> <height>344</height>
</rect> </rect>
</property> </property>
@ -110,37 +110,37 @@
</item> </item>
<item> <item>
<widget class="QCheckBox" name="use_decorations"> <widget class="QCheckBox" name="use_decorations">
<property name="text">
<string>Show checkmarks</string>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Show check marks in the GUI. Values of 'yes', 'checked', and 'true' <string>Show check marks in the GUI. Values of 'yes', 'checked', and 'true'
will show a green check. Values of 'no', 'unchecked', and 'false' will show a red X. will show a green check. Values of 'no', 'unchecked', and 'false' will show a red X.
Everything else will show nothing.</string> Everything else will show nothing.</string>
</property> </property>
<property name="text">
<string>Show checkmarks</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="is_names"> <widget class="QCheckBox" name="is_names">
<property name="text">
<string>Contains names</string>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Check this box if this column contains names, like the authors column.</string> <string>Check this box if this column contains names, like the authors column.</string>
</property> </property>
<property name="text">
<string>Contains names</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="horizontalSpacer_27"> <spacer name="horizontalSpacer_27">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch> <horstretch>10</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
@ -251,25 +251,25 @@ Everything else will show nothing.</string>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="composite_make_category"> <widget class="QCheckBox" name="composite_make_category">
<property name="text">
<string>Show in tags browser</string>
</property>
<property name="toolTip"> <property name="toolTip">
<string>If checked, this column will appear in the tags browser as a category</string> <string>If checked, this column will appear in the tags browser as a category</string>
</property> </property>
<property name="text">
<string>Show in tags browser</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="horizontalSpacer_24"> <spacer name="horizontalSpacer_24">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch> <horstretch>10</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>

View File

@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
import textwrap import textwrap
from functools import partial from functools import partial
from collections import OrderedDict
from PyQt4.Qt import QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget, \ from PyQt4.Qt import QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget, \
QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence, \ QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence, \
@ -18,7 +19,6 @@ from calibre.gui2 import gprefs, min_available_height, available_width, \
warning_dialog warning_dialog
from calibre.gui2.preferences import init_gui, AbortCommit, get_plugin from calibre.gui2.preferences import init_gui, AbortCommit, get_plugin
from calibre.customize.ui import preferences_plugins from calibre.customize.ui import preferences_plugins
from calibre.utils.ordered_dict import OrderedDict
ICON_SIZE = 32 ICON_SIZE = 32

View File

@ -251,7 +251,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if d != 0: if d != 0:
try: try:
validation_formatter.validate(s) validation_formatter.validate(s)
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid template'), error_dialog(self, _('Invalid template'),
'<p>'+_('The template %s is invalid:')%s + \ '<p>'+_('The template %s is invalid:')%s + \
'<br>'+str(err), show=True) '<br>'+str(err), show=True)

View File

@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import textwrap, os import textwrap, os
from collections import OrderedDict
from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \ from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
QBrush QBrush
@ -19,7 +20,6 @@ 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
from calibre.utils.icu import lower from calibre.utils.icu import lower
from calibre.utils.ordered_dict import OrderedDict
class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{ class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{

View File

@ -57,7 +57,7 @@ class SaveTemplate(QWidget, Ui_Form):
return question_dialog(self, _('Constant template'), return question_dialog(self, _('Constant template'),
_('The template contains no {fields}, so all ' _('The template contains no {fields}, so all '
'books will have the same name. Is this OK?')) 'books will have the same name. Is this OK?'))
except Exception, err: except Exception as err:
error_dialog(self, _('Invalid template'), error_dialog(self, _('Invalid template'),
'<p>'+_('The template %s is invalid:')%tmpl + \ '<p>'+_('The template %s is invalid:')%tmpl + \
'<br>'+str(err), show=True) '<br>'+str(err), show=True)

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