mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
9af79ba225
79
recipes/caijing.recipe
Normal file
79
recipes/caijing.recipe
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import re
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
|
||||||
|
class Caijing(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = 'Caijing Magazine'
|
||||||
|
__author__ = 'Eric Chen'
|
||||||
|
|
||||||
|
description = '''Bi-weekly Finance and Economics Review. Founded in 1998, the fortnightly CAIJING
|
||||||
|
Magazine has firmly established itself as a news authority and leading voice for
|
||||||
|
business and financial issues in China.
|
||||||
|
CAIJING Magazine closely tracks the most important aspects of China's economic reforms,
|
||||||
|
developments and policy changes, as well as major events in the capital markets. It also
|
||||||
|
offers a broad international perspective through first-hand reporting on international
|
||||||
|
political and economic issues.
|
||||||
|
CAIJING Magazine is China's most widely read business and finance magazine, with a
|
||||||
|
circulation of 225,000 per issue. It boasts top-level readers from government, business
|
||||||
|
and academic circles. '''
|
||||||
|
language = 'zh'
|
||||||
|
category = 'news, China'
|
||||||
|
encoding = 'UTF-8'
|
||||||
|
timefmt = ' [%a, %d %b, %Y]'
|
||||||
|
needs_subscription = True
|
||||||
|
|
||||||
|
remove_tags = [dict(attrs={'class':['topad', 'nav', 'searchbox', 'connav',
|
||||||
|
'mbx', 'bianji', 'bianji bj', 'lnewlist', 'rdtj', 'loadComment',
|
||||||
|
'conr', 'bottom', 'bottomcopyr', 'emaildy', 'rcom', 'allcontent']}),
|
||||||
|
dict(name=['script', 'noscript', 'style'])]
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
current_issue_url = ""
|
||||||
|
current_issue_cover = ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
br.open('http://service.caijing.com.cn/usermanage/login')
|
||||||
|
br.select_form(name='mainLoginForm')
|
||||||
|
br['username'] = self.username
|
||||||
|
br['password'] = self.password
|
||||||
|
br.submit()
|
||||||
|
return br
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
articles = []
|
||||||
|
soup0 = self.index_to_soup('http://magazine.caijing.com.cn/2011/cjindex2011/')
|
||||||
|
div = soup0.find('div', attrs={'class':'fmcon'})
|
||||||
|
link = div.find('a', href=True)
|
||||||
|
current_issue_url = link['href']
|
||||||
|
|
||||||
|
soup = self.index_to_soup(current_issue_url)
|
||||||
|
|
||||||
|
for div_cover in soup.findAll('img', {'src' : re.compile('.')}):
|
||||||
|
if re.search('\d{4}-\d{2}-\d{2}', div_cover['src']):
|
||||||
|
self.current_issue_cover = div_cover['src']
|
||||||
|
|
||||||
|
feeds = []
|
||||||
|
for section in soup.findAll('div', attrs={'class':'cebd'}):
|
||||||
|
section_title = self.tag_to_string(section.find('div', attrs={'class':'ceti'}))
|
||||||
|
articles = []
|
||||||
|
for post in section.findAll('a', href=True):
|
||||||
|
if re.search('\d{4}-\d{2}-\d{2}', post['href']):
|
||||||
|
date = re.search('\d{4}-\d{2}-\d{2}', post['href']).group(0)
|
||||||
|
id = re.search('\d{9}', post['href']).group(0)
|
||||||
|
url = re.sub(r'\d.*', 'templates/inc/chargecontent2.jsp?id=', post['href'])
|
||||||
|
url = url + id + '&time=' + date + '&cl=106&page=all'
|
||||||
|
|
||||||
|
title = self.tag_to_string(post)
|
||||||
|
articles.append({'title':title, 'url':url, 'date':date})
|
||||||
|
|
||||||
|
if articles:
|
||||||
|
feeds.append((section_title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def get_cover_url(self):
|
||||||
|
return self.current_issue_cover
|
||||||
|
|
@ -8,13 +8,13 @@ __description__ = 'Providing context and clarity on national and international n
|
|||||||
|
|
||||||
'''csmonitor.com'''
|
'''csmonitor.com'''
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
|
||||||
class ChristianScienceMonitor(BasicNewsRecipe):
|
class ChristianScienceMonitor(BasicNewsRecipe):
|
||||||
|
|
||||||
author = 'Kovid Goyal, Sujata Raman and Lorenzo Vigentini'
|
__author__ = 'Kovid Goyal'
|
||||||
description = 'Providing context and clarity on national and international news, peoples and cultures'
|
description = 'Providing context and clarity on national and international news, peoples and cultures'
|
||||||
|
|
||||||
cover_url = 'http://www.csmonitor.com/extension/csm_base/design/csm_design/images/csmlogo_179x46.gif'
|
cover_url = 'http://www.csmonitor.com/extension/csm_base/design/csm_design/images/csmlogo_179x46.gif'
|
||||||
@ -34,6 +34,49 @@ class ChristianScienceMonitor(BasicNewsRecipe):
|
|||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
|
def append_page(self, soup, appendtag, position):
|
||||||
|
nav = soup.find('div',attrs={'class':'navigation'})
|
||||||
|
if nav:
|
||||||
|
pager = nav.findAll('a')
|
||||||
|
for part in pager:
|
||||||
|
if 'Next' in part:
|
||||||
|
nexturl = ('http://www.csmonitor.com' +
|
||||||
|
re.findall(r'href="(.*?)"', str(part))[0])
|
||||||
|
soup2 = self.index_to_soup(nexturl)
|
||||||
|
texttag = soup2.find('div',
|
||||||
|
attrs={'class': re.compile('list-article-.*')})
|
||||||
|
trash_c = soup2.findAll(attrs={'class': 'list-description'})
|
||||||
|
trash_h = soup2.h1
|
||||||
|
for tc in trash_c: tc.extract()
|
||||||
|
trash_h.extract()
|
||||||
|
|
||||||
|
newpos = len(texttag.contents)
|
||||||
|
self.append_page(soup2, texttag, newpos)
|
||||||
|
texttag.extract()
|
||||||
|
appendtag.insert(position, texttag)
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
PRINT_RE = re.compile(r'/layout/set/print/content/view/print/[0-9]*')
|
||||||
|
html = str(soup)
|
||||||
|
try:
|
||||||
|
print_found = PRINT_RE.findall(html)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if print_found:
|
||||||
|
print_url = 'http://www.csmonitor.com' + print_found[0]
|
||||||
|
print_soup = self.index_to_soup(print_url)
|
||||||
|
else:
|
||||||
|
self.append_page(soup, soup.body, 3)
|
||||||
|
|
||||||
|
trash_a = soup.findAll(attrs={'class': re.compile('navigation.*')})
|
||||||
|
trash_b = soup.findAll(attrs={'style': re.compile('.*')})
|
||||||
|
trash_d = soup.findAll(attrs={'class': 'sByline'})
|
||||||
|
for ta in trash_a: ta.extract()
|
||||||
|
for tb in trash_b: tb.extract()
|
||||||
|
for td in trash_d: td.extract()
|
||||||
|
|
||||||
|
print_soup = soup
|
||||||
|
return print_soup
|
||||||
|
|
||||||
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
|
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
|
||||||
[
|
[
|
||||||
@ -43,7 +86,6 @@ class ChristianScienceMonitor(BasicNewsRecipe):
|
|||||||
(r'Full HTML version of this story which may include photos, graphics, and related links.*</body>',
|
(r'Full HTML version of this story which may include photos, graphics, and related links.*</body>',
|
||||||
lambda match : '</body>'),
|
lambda match : '</body>'),
|
||||||
]]
|
]]
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
h1{ color:#000000;font-family: Georgia,Times,"Times New Roman",serif; font-size: large}
|
h1{ color:#000000;font-family: Georgia,Times,"Times New Roman",serif; font-size: large}
|
||||||
.sub{ color:#000000;font-family: Georgia,Times,"Times New Roman",serif; font-size: small;}
|
.sub{ color:#000000;font-family: Georgia,Times,"Times New Roman",serif; font-size: small;}
|
||||||
@ -56,10 +98,9 @@ class ChristianScienceMonitor(BasicNewsRecipe):
|
|||||||
#main{font-family:Arial,Tahoma,Verdana,Helvetica,sans-serif ; font-size: small; }
|
#main{font-family:Arial,Tahoma,Verdana,Helvetica,sans-serif ; font-size: small; }
|
||||||
#photo-details{ font-family:Arial,Helvetica,sans-serif ; color:#999999; font-size: x-small;}
|
#photo-details{ font-family:Arial,Helvetica,sans-serif ; color:#999999; font-size: x-small;}
|
||||||
span.name{color:#205B87;font-family: Georgia,Times,"Times New Roman",serif; font-size: x-small}
|
span.name{color:#205B87;font-family: Georgia,Times,"Times New Roman",serif; font-size: x-small}
|
||||||
p#dateline{color:#444444 ; font-family:Arial,Helvetica,sans-serif ; font-style:italic;}
|
p#dateline{color:#444444 ; font-family:Arial,Helvetica,sans-serif ; font-style:italic;} '''
|
||||||
'''
|
|
||||||
feeds = [
|
feeds = [(u'Top Stories', u'http://rss.csmonitor.com/feeds/top'),
|
||||||
(u'Top Stories' , u'http://rss.csmonitor.com/feeds/top'),
|
|
||||||
(u'World' , u'http://rss.csmonitor.com/feeds/world'),
|
(u'World' , u'http://rss.csmonitor.com/feeds/world'),
|
||||||
(u'USA' , u'http://rss.csmonitor.com/feeds/usa'),
|
(u'USA' , u'http://rss.csmonitor.com/feeds/usa'),
|
||||||
(u'Commentary' , u'http://rss.csmonitor.com/feeds/commentary'),
|
(u'Commentary' , u'http://rss.csmonitor.com/feeds/commentary'),
|
||||||
@ -74,9 +115,7 @@ class ChristianScienceMonitor(BasicNewsRecipe):
|
|||||||
(u'Home Forum' , u'http://rss.csmonitor.com/feeds/homeforum')
|
(u'Home Forum' , u'http://rss.csmonitor.com/feeds/homeforum')
|
||||||
]
|
]
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [dict(name='div', attrs={'id':'mainColumn'}), ]
|
||||||
dict(name='div', attrs={'id':'mainColumn'}),
|
|
||||||
]
|
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'id':['story-tools','videoPlayer','storyRelatedBottom','enlarge-photo','photo-paginate']}),
|
dict(name='div', attrs={'id':['story-tools','videoPlayer','storyRelatedBottom','enlarge-photo','photo-paginate']}),
|
||||||
@ -86,7 +125,10 @@ class ChristianScienceMonitor(BasicNewsRecipe):
|
|||||||
'hide', 'podBrdr']}),
|
'hide', 'podBrdr']}),
|
||||||
dict(name='ul', attrs={'class':[ 'centerliststories']}) ,
|
dict(name='ul', attrs={'class':[ 'centerliststories']}) ,
|
||||||
dict(name='form', attrs={'id':[ 'commentform']}) ,
|
dict(name='form', attrs={'id':[ 'commentform']}) ,
|
||||||
|
dict(name='div', attrs={'class': ['ui-comments']})
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags_after = [ dict(name='div', attrs={'class':[ 'ad csmAd']})]
|
remove_tags_after = [ dict(name='div', attrs={'class':[ 'ad csmAd']}),
|
||||||
|
dict(name='div', attrs={'class': [re.compile('navigation.*')]}),
|
||||||
|
dict(name='div', attrs={'style': [re.compile('.*')]})
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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
44
recipes/hvg.recipe
Normal 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')
|
||||||
|
|
@ -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
11
recipes/planet_kde.recipe
Normal 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')]
|
||||||
|
|
@ -1,4 +1,3 @@
|
|||||||
import re
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
|
||||||
@ -6,55 +5,48 @@ class WashingtonPost(BasicNewsRecipe):
|
|||||||
|
|
||||||
title = 'Washington Post'
|
title = 'Washington Post'
|
||||||
description = 'US political news'
|
description = 'US political news'
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
__author__ = 'Kovid Goyal'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
max_articles_per_feed = 20
|
max_articles_per_feed = 20
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
extra_css = '''
|
feeds = [
|
||||||
#articleCopyright { font-family:Arial,helvetica,sans-serif ; font-weight:bold ; font-size:x-small ;}
|
('Politics', 'http://www.washingtonpost.com/rss/politics'),
|
||||||
p { font-family:"Times New Roman",times,serif ; font-weight:normal ; font-size:small ;}
|
('Nation', 'http://www.washingtonpost.com/rss/national'),
|
||||||
body{font-family:arial,helvetica,sans-serif}
|
('World', 'http://www.washingtonpost.com/rss/world'),
|
||||||
'''
|
('Business', 'http://www.washingtonpost.com/rss/business'),
|
||||||
|
('Lifestyle', 'http://www.washingtonpost.com/rss/lifestyle'),
|
||||||
feeds = [ ('Today\'s Highlights', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/03/24/LI2005032400102.xml'),
|
('Sports', 'http://www.washingtonpost.com/rss/sports'),
|
||||||
('Politics', 'http://www.washingtonpost.com/wp-dyn/rss/politics/index.xml'),
|
('Redskins', 'http://www.washingtonpost.com/rss/sports/redskins'),
|
||||||
('Nation', 'http://www.washingtonpost.com/wp-dyn/rss/nation/index.xml'),
|
('Opinions', 'http://www.washingtonpost.com/rss/opinions'),
|
||||||
('World', 'http://www.washingtonpost.com/wp-dyn/rss/world/index.xml'),
|
('Entertainment', 'http://www.washingtonpost.com/rss/entertainment'),
|
||||||
('Business', 'http://www.washingtonpost.com/wp-dyn/rss/business/index.xml'),
|
('Local', 'http://www.washingtonpost.com/rss/local'),
|
||||||
('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'),
|
('Investigations',
|
||||||
('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'),
|
'http://www.washingtonpost.com/rss/investigations'),
|
||||||
('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'),
|
|
||||||
('Style',
|
|
||||||
'http://www.washingtonpost.com/wp-dyn/rss/print/style/index.xml'),
|
|
||||||
('NFL Sports',
|
|
||||||
'http://www.washingtonpost.com/wp-dyn/rss/sports/index/nfl/index.xml'),
|
|
||||||
('Redskins', 'http://www.washingtonpost.com/wp-dyn/rss/sports/redskins/index.xml'),
|
|
||||||
('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [{'id':['pfmnav', 'ArticleCommentsWrapper']}]
|
remove_tags = [
|
||||||
|
{'class':lambda x: x and 'article-toolbar' in x},
|
||||||
|
{'class':lambda x: x and 'quick-comments' in x},
|
||||||
|
{'class':lambda x: x and 'tweet' in x},
|
||||||
|
{'class':lambda x: x and 'article-related' in x},
|
||||||
|
{'class':lambda x: x and 'hidden' in x.split()},
|
||||||
|
{'class':lambda x: x and 'also-read' in x.split()},
|
||||||
|
{'class':lambda x: x and 'partners-content' in x.split()},
|
||||||
|
{'class':['module share', 'module ads', 'comment-vars', 'hidden',
|
||||||
|
'share-icons-wrap', 'comments']},
|
||||||
|
{'id':['right-rail']},
|
||||||
|
|
||||||
|
]
|
||||||
|
keep_only_tags = dict(id=['content', 'article'])
|
||||||
|
|
||||||
def get_article_url(self, article):
|
|
||||||
return article.get('guid', article.get('link', None))
|
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url.rpartition('.')[0] + '_pf.html'
|
url = url.rpartition('?')[0]
|
||||||
|
return url.replace('_story.html', '_singlePage.html')
|
||||||
|
|
||||||
def postprocess_html(self, soup, first):
|
|
||||||
for div in soup.findAll(name='div', style=re.compile('margin')):
|
|
||||||
div['style'] = ''
|
|
||||||
return soup
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
|
||||||
for tag in soup.findAll('font'):
|
|
||||||
if tag.has_key('size'):
|
|
||||||
if tag['size'] == '+2':
|
|
||||||
if tag.b:
|
|
||||||
return soup
|
|
||||||
return None
|
|
||||||
|
@ -18,6 +18,6 @@ def recipe_title_callback(raw):
|
|||||||
return eval(raw.decode('utf-8'))
|
return eval(raw.decode('utf-8'))
|
||||||
|
|
||||||
vipy.session.add_content_browser('.r', ',r', 'Recipe',
|
vipy.session.add_content_browser('.r', ',r', 'Recipe',
|
||||||
vipy.session.glob_based_iterator(os.path.join(project_dir, 'resources', 'recipes', '*.recipe')),
|
vipy.session.glob_based_iterator(os.path.join(project_dir, 'recipes', '*.recipe')),
|
||||||
vipy.session.regexp_based_matcher(r'title\s*=\s*(?P<title>.+)', 'title', recipe_title_callback))
|
vipy.session.regexp_based_matcher(r'title\s*=\s*(?P<title>.+)', 'title', recipe_title_callback))
|
||||||
EOFPY
|
EOFPY
|
||||||
|
@ -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.
|
||||||
|
@ -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)]
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ __appname__ = 'calibre'
|
|||||||
__version__ = '0.7.50'
|
__version__ = '0.7.50'
|
||||||
__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)
|
||||||
|
@ -2,7 +2,7 @@ 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
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -517,7 +517,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 +575,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)
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -58,12 +58,13 @@ class ANDROID(USBMS):
|
|||||||
0x413c : { 0xb007 : [0x0100, 0x0224]},
|
0x413c : { 0xb007 : [0x0100, 0x0224]},
|
||||||
|
|
||||||
# LG
|
# LG
|
||||||
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100] },
|
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
|
||||||
|
|
||||||
# Archos
|
# Archos
|
||||||
0x0e79 : {
|
0x0e79 : {
|
||||||
0x1400 : [0x0222, 0x0216],
|
0x1400 : [0x0222, 0x0216],
|
||||||
0x1408 : [0x0222, 0x0216],
|
0x1408 : [0x0222, 0x0216],
|
||||||
|
0x1411 : [0x216],
|
||||||
0x1417 : [0x0216],
|
0x1417 : [0x0216],
|
||||||
0x1419 : [0x0216],
|
0x1419 : [0x0216],
|
||||||
0x1420 : [0x0216],
|
0x1420 : [0x0216],
|
||||||
@ -92,14 +93,14 @@ class ANDROID(USBMS):
|
|||||||
|
|
||||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC']
|
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE']
|
||||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||||
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
||||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
|
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
|
||||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
||||||
'7', 'A956', 'A955', 'A43']
|
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM']
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7']
|
'A70S', 'A101IT', '7']
|
||||||
|
@ -748,11 +748,13 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
# Display a dialog recommending using 'Connect to iTunes'
|
# Display a dialog recommending using 'Connect to iTunes'
|
||||||
if not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]:
|
if not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]:
|
||||||
raise OpenFeedback("The recommended connection method for Apple iDevices " +\
|
raise OpenFeedback('<p>' + _('Click the "Connect/Share" button and choose'
|
||||||
"is to use the 'Connect to iTunes' method described in the <br />" +\
|
' "Connect to iTunes" to send books from your calibre library'
|
||||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">Calibre + Apple iDevices FAQ</a>.<br />' +\
|
' to your Apple iDevice.<p>For more information, see'
|
||||||
'After following the Quick Start steps outlined in the FAQ, restart calibre.')
|
'<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.'))
|
||||||
|
|
||||||
# Confirm/create thumbs archive
|
# Confirm/create thumbs archive
|
||||||
if not os.path.exists(self.cache_dir):
|
if not os.path.exists(self.cache_dir):
|
||||||
|
@ -19,7 +19,7 @@ class BLACKBERRY(USBMS):
|
|||||||
|
|
||||||
VENDOR_ID = [0x0fca]
|
VENDOR_ID = [0x0fca]
|
||||||
PRODUCT_ID = [0x8004, 0x0004]
|
PRODUCT_ID = [0x8004, 0x0004]
|
||||||
BCD = [0x0200, 0x0107, 0x0210, 0x0201, 0x0211]
|
BCD = [0x0200, 0x0107, 0x0210, 0x0201, 0x0211, 0x0220]
|
||||||
|
|
||||||
VENDOR_NAME = 'RIM'
|
VENDOR_NAME = 'RIM'
|
||||||
WINDOWS_MAIN_MEM = 'BLACKBERRY_SD'
|
WINDOWS_MAIN_MEM = 'BLACKBERRY_SD'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -49,6 +49,8 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings',
|
|||||||
'dehyphenate', 'renumber_headings',
|
'dehyphenate', 'renumber_headings',
|
||||||
'replace_scene_breaks']
|
'replace_scene_breaks']
|
||||||
|
|
||||||
|
DEFAULT_TRUE_OPTIONS = HEURISTIC_OPTIONS + ['remove_fake_margins']
|
||||||
|
|
||||||
def print_help(parser, log):
|
def print_help(parser, log):
|
||||||
help = parser.format_help().encode(preferred_encoding, 'replace')
|
help = parser.format_help().encode(preferred_encoding, 'replace')
|
||||||
log(help)
|
log(help)
|
||||||
@ -90,7 +92,7 @@ def option_recommendation_to_cli_option(add_option, rec):
|
|||||||
if opt.long_switch == 'verbose':
|
if opt.long_switch == 'verbose':
|
||||||
attrs['action'] = 'count'
|
attrs['action'] = 'count'
|
||||||
attrs.pop('type', '')
|
attrs.pop('type', '')
|
||||||
if opt.name in HEURISTIC_OPTIONS and rec.recommended_value is True:
|
if opt.name in DEFAULT_TRUE_OPTIONS and rec.recommended_value is True:
|
||||||
switches = ['--disable-'+opt.long_switch]
|
switches = ['--disable-'+opt.long_switch]
|
||||||
add_option(Option(*switches, **attrs))
|
add_option(Option(*switches, **attrs))
|
||||||
|
|
||||||
@ -162,6 +164,7 @@ def add_pipeline_options(parser, plumber):
|
|||||||
'chapter', 'chapter_mark',
|
'chapter', 'chapter_mark',
|
||||||
'prefer_metadata_cover', 'remove_first_image',
|
'prefer_metadata_cover', 'remove_first_image',
|
||||||
'insert_metadata', 'page_breaks_before',
|
'insert_metadata', 'page_breaks_before',
|
||||||
|
'remove_fake_margins',
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -304,6 +304,17 @@ OptionRecommendation(name='page_breaks_before',
|
|||||||
'before the specified elements.')
|
'before the specified elements.')
|
||||||
),
|
),
|
||||||
|
|
||||||
|
OptionRecommendation(name='remove_fake_margins',
|
||||||
|
recommended_value=True, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Some documents specify page margins by '
|
||||||
|
'specifying a left and right margin on each individual '
|
||||||
|
'paragraph. calibre will try to detect and remove these '
|
||||||
|
'margins. Sometimes, this can cause the removal of '
|
||||||
|
'margins that should not have been removed. In this '
|
||||||
|
'case you can disable the removal.')
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
OptionRecommendation(name='margin_top',
|
OptionRecommendation(name='margin_top',
|
||||||
recommended_value=5.0, level=OptionRecommendation.LOW,
|
recommended_value=5.0, level=OptionRecommendation.LOW,
|
||||||
help=_('Set the top margin in pts. Default is %default. '
|
help=_('Set the top margin in pts. Default is %default. '
|
||||||
@ -988,9 +999,13 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
page_break_on_body=self.output_plugin.file_type in ('mobi',
|
page_break_on_body=self.output_plugin.file_type in ('mobi',
|
||||||
'lit'))
|
'lit'))
|
||||||
flattener(self.oeb, self.opts)
|
flattener(self.oeb, self.opts)
|
||||||
|
|
||||||
self.opts.insert_blank_line = oibl
|
self.opts.insert_blank_line = oibl
|
||||||
self.opts.remove_paragraph_spacing = orps
|
self.opts.remove_paragraph_spacing = orps
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.transforms.page_margin import RemoveFakeMargins
|
||||||
|
RemoveFakeMargins()(self.oeb, self.log, self.opts)
|
||||||
|
|
||||||
pr(0.9)
|
pr(0.9)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
@ -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
|
||||||
@ -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)
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
@ -1849,22 +1845,7 @@ For lower versions of Python use:
|
|||||||
""" % 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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -575,7 +575,10 @@ class Metadata(object):
|
|||||||
orig_res = res
|
orig_res = res
|
||||||
datatype = cmeta['datatype']
|
datatype = cmeta['datatype']
|
||||||
if datatype == 'text' and cmeta['is_multiple']:
|
if datatype == 'text' and cmeta['is_multiple']:
|
||||||
res = u', '.join(sorted(res, key=sort_key))
|
if cmeta['display'].get('is_names', False):
|
||||||
|
res = u' & '.join(res)
|
||||||
|
else:
|
||||||
|
res = u', '.join(sorted(res, key=sort_key))
|
||||||
elif datatype == 'series' and series_with_index:
|
elif datatype == 'series' and series_with_index:
|
||||||
if self.get_extra(key) is not None:
|
if self.get_extra(key) is not None:
|
||||||
res = res + \
|
res = res + \
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -28,11 +29,12 @@ class Worker(Thread): # {{{
|
|||||||
Get book details from amazons book page in a separate thread
|
Get book details from amazons book page in a separate thread
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, url, result_queue, browser, log, timeout=20):
|
def __init__(self, url, result_queue, browser, log, relevance, plugin, timeout=20):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
self.url, self.result_queue = url, result_queue
|
self.url, self.result_queue = url, result_queue
|
||||||
self.log, self.timeout = log, timeout
|
self.log, self.timeout = log, timeout
|
||||||
|
self.relevance, self.plugin = relevance, plugin
|
||||||
self.browser = browser.clone_browser()
|
self.browser = browser.clone_browser()
|
||||||
self.cover_url = self.amazon_id = self.isbn = None
|
self.cover_url = self.amazon_id = self.isbn = None
|
||||||
|
|
||||||
@ -40,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)
|
||||||
@ -161,6 +163,17 @@ class Worker(Thread): # {{{
|
|||||||
else:
|
else:
|
||||||
self.log.warning('Failed to find product description for url: %r'%self.url)
|
self.log.warning('Failed to find product description for url: %r'%self.url)
|
||||||
|
|
||||||
|
mi.source_relevance = self.relevance
|
||||||
|
|
||||||
|
if self.amazon_id:
|
||||||
|
if self.isbn:
|
||||||
|
self.plugin.cache_isbn_to_identifier(self.isbn, self.amazon_id)
|
||||||
|
if self.cover_url:
|
||||||
|
self.plugin.cache_identifier_to_cover_url(self.amazon_id,
|
||||||
|
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):
|
||||||
@ -266,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'])
|
||||||
|
|
||||||
@ -274,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={}): # {{{
|
||||||
@ -321,6 +335,21 @@ class Amazon(Source):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
def get_cached_cover_url(self, identifiers): # {{{
|
||||||
|
url = None
|
||||||
|
asin = identifiers.get('amazon', None)
|
||||||
|
if asin is None:
|
||||||
|
asin = identifiers.get('asin', None)
|
||||||
|
if asin is None:
|
||||||
|
isbn = identifiers.get('isbn', None)
|
||||||
|
if isbn is not None:
|
||||||
|
asin = self.cached_isbn_to_identifier(isbn)
|
||||||
|
if asin is not None:
|
||||||
|
url = self.cached_identifier_to_cover_url(asin)
|
||||||
|
|
||||||
|
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):
|
||||||
'''
|
'''
|
||||||
@ -335,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)
|
||||||
@ -396,7 +425,8 @@ class Amazon(Source):
|
|||||||
log.error('No matches found with query: %r'%query)
|
log.error('No matches found with query: %r'%query)
|
||||||
return
|
return
|
||||||
|
|
||||||
workers = [Worker(url, result_queue, br, log) for url in matches]
|
workers = [Worker(url, result_queue, br, log, i, self) for i, url in
|
||||||
|
enumerate(matches)]
|
||||||
|
|
||||||
for w in workers:
|
for w in workers:
|
||||||
w.start()
|
w.start()
|
||||||
@ -414,19 +444,47 @@ class Amazon(Source):
|
|||||||
if not a_worker_is_alive:
|
if not a_worker_is_alive:
|
||||||
break
|
break
|
||||||
|
|
||||||
for w in workers:
|
|
||||||
if w.amazon_id:
|
|
||||||
if w.isbn:
|
|
||||||
self.cache_isbn_to_identifier(w.isbn, w.amazon_id)
|
|
||||||
if w.cover_url:
|
|
||||||
self.cache_identifier_to_cover_url(w.amazon_id,
|
|
||||||
w.cover_url)
|
|
||||||
|
|
||||||
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,
|
||||||
@ -472,5 +530,5 @@ if __name__ == '__main__':
|
|||||||
),
|
),
|
||||||
|
|
||||||
])
|
])
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -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,71 @@ 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")
|
||||||
|
prefix_pat = re.compile(r'^(%s)\s+'%("|".join(words)))
|
||||||
|
trailing_paren_pat = re.compile(r'\(.*\)$')
|
||||||
|
whitespace_pat = re.compile(r'\s+')
|
||||||
|
|
||||||
|
def cleanup_title(s):
|
||||||
|
if not s:
|
||||||
|
s = _('Unknown')
|
||||||
|
s = s.strip().lower()
|
||||||
|
s = prefix_pat.sub(' ', s)
|
||||||
|
s = trailing_paren_pat.sub('', s)
|
||||||
|
s = whitespace_pat.sub(' ', s)
|
||||||
|
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):
|
||||||
|
|
||||||
type = _('Metadata source')
|
type = _('Metadata source')
|
||||||
@ -28,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):
|
||||||
@ -55,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:
|
||||||
@ -77,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
|
||||||
@ -128,12 +209,68 @@ class Source(Plugin):
|
|||||||
gr.append(job)
|
gr.append(job)
|
||||||
return [g for g in groups if g]
|
return [g for g in groups if g]
|
||||||
|
|
||||||
|
def test_fields(self, mi):
|
||||||
|
'''
|
||||||
|
Return the first field from self.touched_fields that is null on the
|
||||||
|
mi object
|
||||||
|
'''
|
||||||
|
for key in self.touched_fields:
|
||||||
|
if key.startswith('identifier:'):
|
||||||
|
key = key.partition(':')[-1]
|
||||||
|
if not mi.has_identifier(key):
|
||||||
|
return 'identifier: ' + key
|
||||||
|
elif mi.is_null(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))
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Metadata API {{{
|
# Metadata API {{{
|
||||||
|
|
||||||
|
def get_cached_cover_url(self, identifiers):
|
||||||
|
'''
|
||||||
|
Return cached cover URL for the book identified by
|
||||||
|
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
|
||||||
|
|
||||||
|
def identify_results_keygen(self, title=None, authors=None,
|
||||||
|
identifiers={}):
|
||||||
|
'''
|
||||||
|
Return a function that is used to generate a key that can sort Metadata
|
||||||
|
objects by their relevance given a search query (title, authors,
|
||||||
|
identifiers).
|
||||||
|
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
def keygen(mi):
|
||||||
|
return InternalMetadataCompareKeyGen(mi, self, title, authors,
|
||||||
|
identifiers)
|
||||||
|
return keygen
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -147,6 +284,15 @@ class Source(Plugin):
|
|||||||
the same ISBN/special identifier does not need to get the cover URL
|
the same ISBN/special identifier does not need to get the cover URL
|
||||||
again. Use the caching API for this.
|
again. Use the caching API for this.
|
||||||
|
|
||||||
|
Every Metadata object put into result_queue by this method must have a
|
||||||
|
`source_relevance` attribute that is an integer indicating the order in
|
||||||
|
which the results were returned by the metadata source for this query.
|
||||||
|
This integer will be used by :meth:`compare_identify_results`. If the
|
||||||
|
order is unimportant, set it to zero for every result.
|
||||||
|
|
||||||
|
Make sure that any cover/isbn mapping information is cached before the
|
||||||
|
Metadata object is put into result_queue.
|
||||||
|
|
||||||
:param log: A log object, use it to output debugging information/errors
|
:param log: A log object, use it to output debugging information/errors
|
||||||
:param result_queue: A result Queue, results should be put into it.
|
:param result_queue: A result Queue, results should be put into it.
|
||||||
Each result is a Metadata object
|
Each result is a Metadata object
|
||||||
@ -164,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
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -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,36 +193,78 @@ 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, # {{{
|
||||||
for i in entries:
|
result_queue, timeout):
|
||||||
|
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):
|
||||||
result_queue.put(ans)
|
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)
|
||||||
except:
|
except:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Failed to get metadata for identify entry:',
|
'Failed to get metadata for identify entry:',
|
||||||
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):
|
||||||
@ -212,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)
|
||||||
|
|
||||||
@ -221,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)
|
||||||
|
|
||||||
@ -237,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)
|
||||||
@ -252,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)]
|
||||||
#),
|
),
|
||||||
])
|
])
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
107
src/calibre/ebooks/metadata/sources/identify.py
Normal file
107
src/calibre/ebooks/metadata/sources/identify.py
Normal 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)
|
||||||
|
|
35
src/calibre/ebooks/metadata/sources/openlibrary.py
Normal file
35
src/calibre/ebooks/metadata/sources/openlibrary.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
@ -46,15 +45,6 @@ def authors_test(authors):
|
|||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
def _test_fields(touched_fields, mi):
|
|
||||||
for key in touched_fields:
|
|
||||||
if key.startswith('identifier:'):
|
|
||||||
key = key.partition(':')[-1]
|
|
||||||
if not mi.has_identifier(key):
|
|
||||||
return 'identifier: ' + key
|
|
||||||
elif mi.is_null(key):
|
|
||||||
return key
|
|
||||||
|
|
||||||
|
|
||||||
def test_identify_plugin(name, tests):
|
def test_identify_plugin(name, tests):
|
||||||
'''
|
'''
|
||||||
@ -99,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:
|
||||||
@ -120,13 +118,42 @@ def test_identify_plugin(name, tests):
|
|||||||
prints('Log saved to', lf)
|
prints('Log saved to', lf)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
good = [x for x in possibles if _test_fields(plugin.touched_fields, x) is
|
good = [x for x in possibles if plugin.test_fields(x) is
|
||||||
None]
|
None]
|
||||||
if not good:
|
if not good:
|
||||||
prints('Failed to find', _test_fields(plugin.touched_fields,
|
prints('Failed to find', plugin.test_fields(possibles[0]))
|
||||||
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))
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 = ''
|
||||||
|
@ -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
|
||||||
|
153
src/calibre/ebooks/oeb/transforms/page_margin.py
Normal file
153
src/calibre/ebooks/oeb/transforms/page_margin.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#!/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 collections import Counter
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath
|
||||||
|
|
||||||
|
class RemoveFakeMargins(object):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Remove left and right margins from paragraph/divs if the same margin is specified
|
||||||
|
on almost all the elements of at that level.
|
||||||
|
|
||||||
|
Must be called only after CSS flattening
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __call__(self, oeb, log, opts):
|
||||||
|
if not opts.remove_fake_margins:
|
||||||
|
return
|
||||||
|
self.oeb, self.log, self.opts = oeb, log, opts
|
||||||
|
stylesheet = None
|
||||||
|
self.levels = {}
|
||||||
|
self.stats = {}
|
||||||
|
self.selector_map = {}
|
||||||
|
|
||||||
|
for item in self.oeb.manifest:
|
||||||
|
if item.media_type.lower() in OEB_STYLES:
|
||||||
|
stylesheet = item
|
||||||
|
break
|
||||||
|
if stylesheet is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log('Removing fake margins...')
|
||||||
|
|
||||||
|
stylesheet = stylesheet.data
|
||||||
|
|
||||||
|
from cssutils.css import CSSRule
|
||||||
|
for rule in stylesheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
|
||||||
|
self.selector_map[rule.selectorList.selectorText] = rule.style
|
||||||
|
|
||||||
|
self.find_levels()
|
||||||
|
|
||||||
|
for level in self.levels:
|
||||||
|
self.process_level(level)
|
||||||
|
|
||||||
|
def get_margins(self, elem):
|
||||||
|
cls = elem.get('class', None)
|
||||||
|
if cls:
|
||||||
|
style = self.selector_map.get('.'+cls, None)
|
||||||
|
if style:
|
||||||
|
return style.marginLeft, style.marginRight, style
|
||||||
|
return '', '', None
|
||||||
|
|
||||||
|
|
||||||
|
def process_level(self, level):
|
||||||
|
elems = self.levels[level]
|
||||||
|
self.stats[level+'_left'] = Counter()
|
||||||
|
self.stats[level+'_right'] = Counter()
|
||||||
|
|
||||||
|
for elem in elems:
|
||||||
|
lm, rm = self.get_margins(elem)[:2]
|
||||||
|
self.stats[level+'_left'][lm] += 1
|
||||||
|
self.stats[level+'_right'][rm] += 1
|
||||||
|
|
||||||
|
self.log.debug(level, ' left margin stats:', self.stats[level+'_left'])
|
||||||
|
self.log.debug(level, ' right margin stats:', self.stats[level+'_right'])
|
||||||
|
|
||||||
|
remove_left = self.analyze_stats(self.stats[level+'_left'])
|
||||||
|
remove_right = self.analyze_stats(self.stats[level+'_right'])
|
||||||
|
|
||||||
|
|
||||||
|
if remove_left:
|
||||||
|
mcl = self.stats[level+'_left'].most_common(1)[0][0]
|
||||||
|
self.log('Removing level %s left margin of:'%level, mcl)
|
||||||
|
|
||||||
|
if remove_right:
|
||||||
|
mcr = self.stats[level+'_right'].most_common(1)[0][0]
|
||||||
|
self.log('Removing level %s right margin of:'%level, mcr)
|
||||||
|
|
||||||
|
if remove_left or remove_right:
|
||||||
|
for elem in elems:
|
||||||
|
lm, rm, style = self.get_margins(elem)
|
||||||
|
if remove_left and lm == mcl:
|
||||||
|
style.removeProperty('margin-left')
|
||||||
|
if remove_right and rm == mcr:
|
||||||
|
style.removeProperty('margin-right')
|
||||||
|
|
||||||
|
def find_levels(self):
|
||||||
|
|
||||||
|
def level_of(elem, body):
|
||||||
|
ans = 1
|
||||||
|
while elem.getparent() is not body:
|
||||||
|
ans += 1
|
||||||
|
elem = elem.getparent()
|
||||||
|
return ans
|
||||||
|
|
||||||
|
paras = XPath('descendant::h:p|descendant::h:div')
|
||||||
|
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
body = XPath('//h:body')(item.data)
|
||||||
|
if not body:
|
||||||
|
continue
|
||||||
|
body = body[0]
|
||||||
|
|
||||||
|
for p in paras(body):
|
||||||
|
level = level_of(p, body)
|
||||||
|
level = '%s_%d'%(barename(p.tag), level)
|
||||||
|
if level not in self.levels:
|
||||||
|
self.levels[level] = []
|
||||||
|
self.levels[level].append(p)
|
||||||
|
|
||||||
|
remove = set()
|
||||||
|
for k, v in self.levels.iteritems():
|
||||||
|
num = len(v)
|
||||||
|
self.log.debug('Found %d items of level:'%num, k)
|
||||||
|
level = int(k.split('_')[-1])
|
||||||
|
tag = k.split('_')[0]
|
||||||
|
if tag == 'p' and num < 25:
|
||||||
|
remove.add(k)
|
||||||
|
if tag == 'div':
|
||||||
|
if level > 2 and num < 25:
|
||||||
|
remove.add(k)
|
||||||
|
elif level < 3:
|
||||||
|
# Check each level < 3 element and only keep those
|
||||||
|
# that have many child paras
|
||||||
|
for elem in list(v):
|
||||||
|
children = len(paras(elem))
|
||||||
|
if children < 5:
|
||||||
|
v.remove(elem)
|
||||||
|
|
||||||
|
for k in remove:
|
||||||
|
self.levels.pop(k)
|
||||||
|
self.log.debug('Ignoring level', k)
|
||||||
|
|
||||||
|
def analyze_stats(self, stats):
|
||||||
|
if not stats:
|
||||||
|
return False
|
||||||
|
mc = stats.most_common(1)
|
||||||
|
if len(mc) > 1:
|
||||||
|
return False
|
||||||
|
mc = mc[0]
|
||||||
|
most_common, most_common_count = mc
|
||||||
|
if not most_common or most_common == '0':
|
||||||
|
return False
|
||||||
|
total = sum(stats.values())
|
||||||
|
# True if greater than 95% of elements have the same margin
|
||||||
|
return most_common_count/total > 0.95
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -22,6 +22,7 @@ border_style_map = {
|
|||||||
'dot-dot-dash': 'dotted',
|
'dot-dot-dash': 'dotted',
|
||||||
'outset': 'outset',
|
'outset': 'outset',
|
||||||
'tripple': 'double',
|
'tripple': 'double',
|
||||||
|
'triple': 'double',
|
||||||
'thick-thin-small': 'solid',
|
'thick-thin-small': 'solid',
|
||||||
'thin-thick-small': 'solid',
|
'thin-thick-small': 'solid',
|
||||||
'thin-thick-thin-small': 'solid',
|
'thin-thick-thin-small': 'solid',
|
||||||
@ -267,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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
PyTextile
|
PyTextile
|
||||||
|
|
||||||
@ -202,26 +204,27 @@ class Textile(object):
|
|||||||
(re.compile(r'{Rs}'), r'₨'), # Rupee
|
(re.compile(r'{Rs}'), r'₨'), # Rupee
|
||||||
(re.compile(r'{(C=|=C)}'), r'€'), # euro
|
(re.compile(r'{(C=|=C)}'), r'€'), # euro
|
||||||
(re.compile(r'{tm}'), r'™'), # trademark
|
(re.compile(r'{tm}'), r'™'), # trademark
|
||||||
(re.compile(r'{spade}'), r'♠'), # spade
|
(re.compile(r'{spades?}'), r'♠'), # spade
|
||||||
(re.compile(r'{club}'), r'♣'), # club
|
(re.compile(r'{clubs?}'), r'♣'), # club
|
||||||
(re.compile(r'{heart}'), r'♥'), # heart
|
(re.compile(r'{hearts?}'), r'♥'), # heart
|
||||||
(re.compile(r'{diamond}'), r'♦'), # diamond
|
(re.compile(r'{diam(onds?|s)}'), r'♦'), # diamond
|
||||||
|
(re.compile(r'{"}'), r'"'), # double-quote
|
||||||
|
(re.compile(r"{'}"), r'''), # single-quote
|
||||||
|
(re.compile(r"{(’|'/|/')}"), r'’'), # closing-single-quote - apostrophe
|
||||||
|
(re.compile(r"{(‘|\\'|'\\)}"), r'‘'), # opening-single-quote
|
||||||
|
(re.compile(r'{(”|"/|/")}'), r'”'), # closing-double-quote
|
||||||
|
(re.compile(r'{(“|\\"|"\\)}'), r'“'), # opening-double-quote
|
||||||
]
|
]
|
||||||
glyph_defaults = [
|
glyph_defaults = [
|
||||||
(re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), r'\1\2×\3'), # dimension sign
|
(re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), r'\1\2×\3'), # dimension sign
|
||||||
(re.compile(r'(\d+)\'', re.I), r'\1′'), # prime
|
(re.compile(r'(\d+)\'', re.I), r'\1′'), # prime
|
||||||
(re.compile(r'(\d+)\"', re.I), r'\1″'), # prime-double
|
(re.compile(r'(\d+)\"', re.I), r'\1″'), # prime-double
|
||||||
(re.compile(r"(\w)\'(\w)"), r'\1’\2'), # apostrophe's
|
|
||||||
(re.compile(r'(\s)\'(\d+\w?)\b(?!\')'), r'\1’\2'), # back in '88
|
|
||||||
(re.compile(r'(\S)\'(?=\s|\'|<|$)'), r'\1’'), # single closing
|
|
||||||
(re.compile(r'\'/'), r'‘'), # single opening
|
|
||||||
(re.compile(r'(\")\"'), r'\1”'), # double closing - following another
|
|
||||||
(re.compile(r'(\S)\"(?=\s|”|<|$)'), r'\1”'), # double closing
|
|
||||||
(re.compile(r'"'), r'“'), # double opening
|
|
||||||
(re.compile(r'\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])'), r'<acronym title="\2">\1</acronym>'), # 3+ uppercase acronym
|
(re.compile(r'\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])'), r'<acronym title="\2">\1</acronym>'), # 3+ uppercase acronym
|
||||||
(re.compile(r'\b([A-Z][A-Z\'\-]+[A-Z])(?=[\s.,\)>])'), r'<span class="caps">\1</span>'), # 3+ uppercase
|
(re.compile(r'\b([A-Z][A-Z\'\-]+[A-Z])(?=[\s.,\)>])'), r'<span class="caps">\1</span>'), # 3+ uppercase
|
||||||
(re.compile(r'\b(\s{0,1})?\.{3}'), r'\1⁄'), # ellipsis
|
(re.compile(r'\b(\s{0,1})?\.{3}'), r'\1…'), # ellipsis
|
||||||
(re.compile(r'(\s?)--(\s?)'), r'\1—\2'), # em dash
|
(re.compile(r'^[\*_-]{3,}$', re.M), r'<hr />'), # <hr> scene-break
|
||||||
|
(re.compile(r'\b--\b'), r'—'), # em dash
|
||||||
|
(re.compile(r'(\s)--(\s)'), r'\1—\2'), # em dash
|
||||||
(re.compile(r'\s-(?:\s|$)'), r' – '), # en dash
|
(re.compile(r'\s-(?:\s|$)'), r' – '), # en dash
|
||||||
(re.compile(r'\b( ?)[([]TM[])]', re.I), r'\1™'), # trademark
|
(re.compile(r'\b( ?)[([]TM[])]', re.I), r'\1™'), # trademark
|
||||||
(re.compile(r'\b( ?)[([]R[])]', re.I), r'\1®'), # registered
|
(re.compile(r'\b( ?)[([]R[])]', re.I), r'\1®'), # registered
|
||||||
@ -747,7 +750,7 @@ class Textile(object):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
def shelve(self, text):
|
def shelve(self, text):
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4()) + 'c'
|
||||||
self.shelf[id] = text
|
self.shelf[id] = text
|
||||||
return id
|
return id
|
||||||
|
|
||||||
@ -865,11 +868,11 @@ class Textile(object):
|
|||||||
'hello <span class="bob">span <strong>strong</strong> and <b>bold</b></span> goodbye'
|
'hello <span class="bob">span <strong>strong</strong> and <b>bold</b></span> goodbye'
|
||||||
"""
|
"""
|
||||||
qtags = (r'\*\*', r'\*', r'\?\?', r'\-', r'__', r'_', r'%', r'\+', r'~', r'\^')
|
qtags = (r'\*\*', r'\*', r'\?\?', r'\-', r'__', r'_', r'%', r'\+', r'~', r'\^')
|
||||||
pnct = ".,\"'?!;:()"
|
pnct = ".,\"'?!;:"
|
||||||
|
|
||||||
for qtag in qtags:
|
for qtag in qtags:
|
||||||
pattern = re.compile(r"""
|
pattern = re.compile(r"""
|
||||||
(?:^|(?<=[\s>%(pnct)s])|\[|([\]}]))
|
(?:^|(?<=[\s>%(pnct)s\(])|\[|([\]}]))
|
||||||
(%(qtag)s)(?!%(qtag)s)
|
(%(qtag)s)(?!%(qtag)s)
|
||||||
(%(c)s)
|
(%(c)s)
|
||||||
(?::(\S+))?
|
(?::(\S+))?
|
||||||
|
@ -165,6 +165,7 @@ 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)
|
||||||
|
@ -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)
|
||||||
|
@ -25,8 +25,11 @@ class PreferencesAction(InterfaceAction):
|
|||||||
self.gui.run_wizard)
|
self.gui.run_wizard)
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
pm.addSeparator()
|
pm.addSeparator()
|
||||||
pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
|
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
|
||||||
self.debug_restart)
|
self.debug_restart)
|
||||||
|
ac.setShortcut('Ctrl+Shift+R')
|
||||||
|
self.gui.addAction(ac)
|
||||||
|
|
||||||
self.qaction.setMenu(pm)
|
self.qaction.setMenu(pm)
|
||||||
self.preferences_menu = pm
|
self.preferences_menu = pm
|
||||||
for x in (self.gui.preferences_action, self.qaction):
|
for x in (self.gui.preferences_action, self.qaction):
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.')
|
||||||
|
@ -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_()
|
||||||
|
@ -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
|
||||||
|
@ -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.')
|
||||||
|
@ -21,7 +21,7 @@ class StructureDetectionWidget(Widget, Ui_Form):
|
|||||||
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,
|
Widget.__init__(self, parent,
|
||||||
['chapter', 'chapter_mark',
|
['chapter', 'chapter_mark',
|
||||||
'remove_first_image',
|
'remove_first_image', 'remove_fake_margins',
|
||||||
'insert_metadata', 'page_breaks_before']
|
'insert_metadata', 'page_breaks_before']
|
||||||
)
|
)
|
||||||
self.db, self.book_id = db, book_id
|
self.db, self.book_id = db, book_id
|
||||||
|
@ -48,10 +48,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="3">
|
<item row="7" column="0" colspan="3">
|
||||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0" colspan="3">
|
<item row="8" column="0" colspan="3">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0" colspan="3">
|
<item row="5" column="0" colspan="3">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field.</string>
|
<string>The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field.</string>
|
||||||
@ -87,6 +87,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QCheckBox" name="opt_remove_fake_margins">
|
||||||
|
<property name="text">
|
||||||
|
<string>Remove &fake margins</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -226,10 +226,18 @@ class Comments(Base):
|
|||||||
class Text(Base):
|
class Text(Base):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
|
if self.col_metadata['display'].get('is_names', False):
|
||||||
|
self.sep = u' & '
|
||||||
|
else:
|
||||||
|
self.sep = u', '
|
||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
w = MultiCompleteLineEdit(parent)
|
w = MultiCompleteLineEdit(parent)
|
||||||
|
w.set_separator(self.sep.strip())
|
||||||
|
if self.sep == u' & ':
|
||||||
|
w.set_space_before_sep(True)
|
||||||
|
w.set_add_separator(tweaks['authors_completer_append_separator'])
|
||||||
w.update_items_cache(values)
|
w.update_items_cache(values)
|
||||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||||
else:
|
else:
|
||||||
@ -261,12 +269,12 @@ class Text(Base):
|
|||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
if not val:
|
if not val:
|
||||||
val = []
|
val = []
|
||||||
self.widgets[1].setText(u', '.join(val))
|
self.widgets[1].setText(self.sep.join(val))
|
||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
val = unicode(self.widgets[1].text()).strip()
|
val = unicode(self.widgets[1].text()).strip()
|
||||||
ans = [x.strip() for x in val.split(',') if x.strip()]
|
ans = [x.strip() for x in val.split(self.sep.strip()) if x.strip()]
|
||||||
if not ans:
|
if not ans:
|
||||||
ans = None
|
ans = None
|
||||||
return ans
|
return ans
|
||||||
@ -847,13 +855,20 @@ class BulkText(BulkBase):
|
|||||||
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||||
self.adding_widget = self.main_widget
|
self.adding_widget = self.main_widget
|
||||||
|
|
||||||
w = RemoveTags(parent, values)
|
if not self.col_metadata['display'].get('is_names', False):
|
||||||
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
w = RemoveTags(parent, values)
|
||||||
_('tags to remove'), parent))
|
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
||||||
self.widgets.append(w)
|
_('tags to remove'), parent))
|
||||||
self.removing_widget = w
|
self.widgets.append(w)
|
||||||
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
self.removing_widget = w
|
||||||
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
self.main_widget.set_separator(',')
|
||||||
|
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
else:
|
||||||
|
self.main_widget.set_separator('&')
|
||||||
|
self.main_widget.set_space_before_sep(True)
|
||||||
|
self.main_widget.set_add_separator(
|
||||||
|
tweaks['authors_completer_append_separator'])
|
||||||
else:
|
else:
|
||||||
self.make_widgets(parent, MultiCompleteComboBox)
|
self.make_widgets(parent, MultiCompleteComboBox)
|
||||||
self.main_widget.set_separator(None)
|
self.main_widget.set_separator(None)
|
||||||
@ -882,21 +897,26 @@ class BulkText(BulkBase):
|
|||||||
if not self.a_c_checkbox.isChecked():
|
if not self.a_c_checkbox.isChecked():
|
||||||
return
|
return
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
remove_all, adding, rtext = self.gui_val
|
if self.col_metadata['display'].get('is_names', False):
|
||||||
remove = set()
|
val = self.gui_val
|
||||||
if remove_all:
|
add = [v.strip() for v in val.split('&') if v.strip()]
|
||||||
remove = set(self.db.all_custom(num=self.col_id))
|
self.db.set_custom_bulk(book_ids, add, num=self.col_id)
|
||||||
else:
|
else:
|
||||||
txt = rtext
|
remove_all, adding, rtext = self.gui_val
|
||||||
|
remove = set()
|
||||||
|
if remove_all:
|
||||||
|
remove = set(self.db.all_custom(num=self.col_id))
|
||||||
|
else:
|
||||||
|
txt = rtext
|
||||||
|
if txt:
|
||||||
|
remove = set([v.strip() for v in txt.split(',')])
|
||||||
|
txt = adding
|
||||||
if txt:
|
if txt:
|
||||||
remove = set([v.strip() for v in txt.split(',')])
|
add = set([v.strip() for v in txt.split(',')])
|
||||||
txt = adding
|
else:
|
||||||
if txt:
|
add = set()
|
||||||
add = set([v.strip() for v in txt.split(',')])
|
self.db.set_custom_bulk_multiple(book_ids, add=add,
|
||||||
else:
|
remove=remove, num=self.col_id)
|
||||||
add = set()
|
|
||||||
self.db.set_custom_bulk_multiple(book_ids, add=add, remove=remove,
|
|
||||||
num=self.col_id)
|
|
||||||
else:
|
else:
|
||||||
val = self.gui_val
|
val = self.gui_val
|
||||||
val = self.normalize_ui_val(val)
|
val = self.normalize_ui_val(val)
|
||||||
@ -905,10 +925,11 @@ class BulkText(BulkBase):
|
|||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
return self.removing_widget.checkbox.isChecked(), \
|
if not self.col_metadata['display'].get('is_names', False):
|
||||||
unicode(self.adding_widget.text()), \
|
return self.removing_widget.checkbox.isChecked(), \
|
||||||
unicode(self.removing_widget.tags_box.text())
|
unicode(self.adding_widget.text()), \
|
||||||
|
unicode(self.removing_widget.tags_box.text())
|
||||||
|
return unicode(self.adding_widget.text())
|
||||||
val = unicode(self.main_widget.currentText()).strip()
|
val = unicode(self.main_widget.currentText()).strip()
|
||||||
if not val:
|
if not val:
|
||||||
val = None
|
val = None
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
@ -653,7 +653,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
|
|
||||||
if self.destination_field_fm['is_multiple']:
|
if self.destination_field_fm['is_multiple']:
|
||||||
if self.comma_separated.isChecked():
|
if self.comma_separated.isChecked():
|
||||||
if dest == 'authors':
|
if dest == 'authors' or \
|
||||||
|
(self.destination_field_fm['is_custom'] and
|
||||||
|
self.destination_field_fm['datatype'] == 'text' and
|
||||||
|
self.destination_field_fm['display'].get('is_names', False)):
|
||||||
splitter = ' & '
|
splitter = ' & '
|
||||||
else:
|
else:
|
||||||
splitter = ','
|
splitter = ','
|
||||||
|
@ -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_()
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -640,18 +640,18 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return self.bool_yes_icon
|
return self.bool_yes_icon
|
||||||
return self.bool_blank_icon
|
return self.bool_blank_icon
|
||||||
|
|
||||||
def text_type(r, mult=False, idx=-1):
|
def text_type(r, mult=None, idx=-1):
|
||||||
text = self.db.data[r][idx]
|
text = self.db.data[r][idx]
|
||||||
if text and mult:
|
if text and mult is not None:
|
||||||
return QVariant(', '.join(sorted(text.split('|'),key=sort_key)))
|
if mult:
|
||||||
|
return QVariant(u' & '.join(text.split('|')))
|
||||||
|
return QVariant(u', '.join(sorted(text.split('|'),key=sort_key)))
|
||||||
return QVariant(text)
|
return QVariant(text)
|
||||||
|
|
||||||
def decorated_text_type(r, mult=False, idx=-1):
|
def decorated_text_type(r, idx=-1):
|
||||||
text = self.db.data[r][idx]
|
text = self.db.data[r][idx]
|
||||||
if force_to_bool(text) is not None:
|
if force_to_bool(text) is not None:
|
||||||
return None
|
return None
|
||||||
if text and mult:
|
|
||||||
return QVariant(', '.join(sorted(text.split('|'),key=sort_key)))
|
|
||||||
return QVariant(text)
|
return QVariant(text)
|
||||||
|
|
||||||
def number_type(r, idx=-1):
|
def number_type(r, idx=-1):
|
||||||
@ -659,7 +659,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
self.dc = {
|
self.dc = {
|
||||||
'title' : functools.partial(text_type,
|
'title' : functools.partial(text_type,
|
||||||
idx=self.db.field_metadata['title']['rec_index'], mult=False),
|
idx=self.db.field_metadata['title']['rec_index'], mult=None),
|
||||||
'authors' : functools.partial(authors,
|
'authors' : functools.partial(authors,
|
||||||
idx=self.db.field_metadata['authors']['rec_index']),
|
idx=self.db.field_metadata['authors']['rec_index']),
|
||||||
'size' : functools.partial(size,
|
'size' : functools.partial(size,
|
||||||
@ -671,14 +671,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
'rating' : functools.partial(rating_type,
|
'rating' : functools.partial(rating_type,
|
||||||
idx=self.db.field_metadata['rating']['rec_index']),
|
idx=self.db.field_metadata['rating']['rec_index']),
|
||||||
'publisher': functools.partial(text_type,
|
'publisher': functools.partial(text_type,
|
||||||
idx=self.db.field_metadata['publisher']['rec_index'], mult=False),
|
idx=self.db.field_metadata['publisher']['rec_index'], mult=None),
|
||||||
'tags' : functools.partial(tags,
|
'tags' : functools.partial(tags,
|
||||||
idx=self.db.field_metadata['tags']['rec_index']),
|
idx=self.db.field_metadata['tags']['rec_index']),
|
||||||
'series' : functools.partial(series_type,
|
'series' : functools.partial(series_type,
|
||||||
idx=self.db.field_metadata['series']['rec_index'],
|
idx=self.db.field_metadata['series']['rec_index'],
|
||||||
siix=self.db.field_metadata['series_index']['rec_index']),
|
siix=self.db.field_metadata['series_index']['rec_index']),
|
||||||
'ondevice' : functools.partial(text_type,
|
'ondevice' : functools.partial(text_type,
|
||||||
idx=self.db.field_metadata['ondevice']['rec_index'], mult=False),
|
idx=self.db.field_metadata['ondevice']['rec_index'], mult=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dc_decorator = {
|
self.dc_decorator = {
|
||||||
@ -692,11 +692,12 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
datatype = self.custom_columns[col]['datatype']
|
datatype = self.custom_columns[col]['datatype']
|
||||||
if datatype in ('text', 'comments', 'composite', 'enumeration'):
|
if datatype in ('text', 'comments', 'composite', 'enumeration'):
|
||||||
mult=self.custom_columns[col]['is_multiple']
|
mult=self.custom_columns[col]['is_multiple']
|
||||||
|
if mult is not None:
|
||||||
|
mult = self.custom_columns[col]['display'].get('is_names', False)
|
||||||
self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
|
self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
|
||||||
if datatype in ['text', 'composite', 'enumeration'] and not mult:
|
if datatype in ['text', 'composite', 'enumeration'] and not mult:
|
||||||
if self.custom_columns[col]['display'].get('use_decorations', False):
|
if self.custom_columns[col]['display'].get('use_decorations', False):
|
||||||
self.dc[col] = functools.partial(decorated_text_type,
|
self.dc[col] = functools.partial(decorated_text_type, idx=idx)
|
||||||
idx=idx, mult=mult)
|
|
||||||
self.dc_decorator[col] = functools.partial(
|
self.dc_decorator[col] = functools.partial(
|
||||||
bool_type_decorator, idx=idx,
|
bool_type_decorator, idx=idx,
|
||||||
bool_cols_are_tristate=
|
bool_cols_are_tristate=
|
||||||
|
@ -78,6 +78,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.pubdate_delegate = PubDateDelegate(self)
|
self.pubdate_delegate = PubDateDelegate(self)
|
||||||
self.tags_delegate = CompleteDelegate(self, ',', 'all_tags')
|
self.tags_delegate = CompleteDelegate(self, ',', 'all_tags')
|
||||||
self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
|
self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
|
||||||
|
self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True)
|
||||||
self.series_delegate = TextDelegate(self)
|
self.series_delegate = TextDelegate(self)
|
||||||
self.publisher_delegate = TextDelegate(self)
|
self.publisher_delegate = TextDelegate(self)
|
||||||
self.text_delegate = TextDelegate(self)
|
self.text_delegate = TextDelegate(self)
|
||||||
@ -410,6 +411,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.save_state()
|
self.save_state()
|
||||||
self._model.set_database(db)
|
self._model.set_database(db)
|
||||||
self.tags_delegate.set_database(db)
|
self.tags_delegate.set_database(db)
|
||||||
|
self.cc_names_delegate.set_database(db)
|
||||||
self.authors_delegate.set_database(db)
|
self.authors_delegate.set_database(db)
|
||||||
self.series_delegate.set_auto_complete_function(db.all_series)
|
self.series_delegate.set_auto_complete_function(db.all_series)
|
||||||
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
||||||
@ -431,12 +433,17 @@ class BooksView(QTableView): # {{{
|
|||||||
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
||||||
elif cc['datatype'] == 'comments':
|
elif cc['datatype'] == 'comments':
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
||||||
elif cc['datatype'] in ('text', 'series'):
|
elif cc['datatype'] == 'text':
|
||||||
if cc['is_multiple']:
|
if cc['is_multiple']:
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
if cc['display'].get('is_names', False):
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead),
|
||||||
|
self.cc_names_delegate)
|
||||||
|
else:
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead),
|
||||||
|
self.tags_delegate)
|
||||||
else:
|
else:
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
elif cc['datatype'] in ('int', 'float'):
|
elif cc['datatype'] in ('series', 'int', 'float'):
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
elif cc['datatype'] == 'bool':
|
elif cc['datatype'] == 'bool':
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -63,7 +63,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
for col, name in [('isbn', _('ISBN')), ('formats', _('Formats')),
|
for col, name in [('isbn', _('ISBN')), ('formats', _('Formats')),
|
||||||
('last_modified', _('Modified Date')), ('yesno', _('Yes/No')),
|
('last_modified', _('Modified Date')), ('yesno', _('Yes/No')),
|
||||||
('tags', _('Tags')), ('series', _('Series')), ('rating',
|
('tags', _('Tags')), ('series', _('Series')), ('rating',
|
||||||
_('Rating'))]:
|
_('Rating')), ('people', _("People's names"))]:
|
||||||
text += ' <a href="col:%s">%s</a>,'%(col, name)
|
text += ' <a href="col:%s">%s</a>,'%(col, name)
|
||||||
text = text[:-1]
|
text = text[:-1]
|
||||||
self.shortcuts.setText(text)
|
self.shortcuts.setText(text)
|
||||||
@ -125,6 +125,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
self.datatype_changed()
|
self.datatype_changed()
|
||||||
if ct in ['text', 'composite', 'enumeration']:
|
if ct in ['text', 'composite', 'enumeration']:
|
||||||
self.use_decorations.setChecked(c['display'].get('use_decorations', False))
|
self.use_decorations.setChecked(c['display'].get('use_decorations', False))
|
||||||
|
elif ct == '*text':
|
||||||
|
self.is_names.setChecked(c['display'].get('is_names', False))
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def shortcut_activated(self, url):
|
def shortcut_activated(self, url):
|
||||||
@ -134,6 +136,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
'tags' : 1,
|
'tags' : 1,
|
||||||
'series': 3,
|
'series': 3,
|
||||||
'rating': 8,
|
'rating': 8,
|
||||||
|
'people': 1,
|
||||||
}.get(which, 10))
|
}.get(which, 10))
|
||||||
self.column_name_box.setText(which)
|
self.column_name_box.setText(which)
|
||||||
self.column_heading_box.setText({
|
self.column_heading_box.setText({
|
||||||
@ -143,7 +146,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
'tags': _('My Tags'),
|
'tags': _('My Tags'),
|
||||||
'series': _('My Series'),
|
'series': _('My Series'),
|
||||||
'rating': _('My Rating'),
|
'rating': _('My Rating'),
|
||||||
'last_modified':_('Modified Date')}[which])
|
'last_modified':_('Modified Date'),
|
||||||
|
'people': _('People')}[which])
|
||||||
|
self.is_names.setChecked(which == 'people')
|
||||||
if self.composite_box.isVisible():
|
if self.composite_box.isVisible():
|
||||||
self.composite_box.setText(
|
self.composite_box.setText(
|
||||||
{
|
{
|
||||||
@ -153,7 +158,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
}[which])
|
}[which])
|
||||||
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
|
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
|
||||||
|
|
||||||
|
|
||||||
def datatype_changed(self, *args):
|
def datatype_changed(self, *args):
|
||||||
try:
|
try:
|
||||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
||||||
@ -167,6 +171,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
for x in ('box', 'default_label', 'label'):
|
for x in ('box', 'default_label', 'label'):
|
||||||
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
|
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
|
||||||
self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration'])
|
self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration'])
|
||||||
|
self.is_names.setVisible(col_type == '*text')
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
col = unicode(self.column_name_box.text()).strip()
|
col = unicode(self.column_name_box.text()).strip()
|
||||||
@ -241,6 +246,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
return self.simple_error('', _('The value "{0}" is in the '
|
return self.simple_error('', _('The value "{0}" is in the '
|
||||||
'list more than once').format(l[i]))
|
'list more than once').format(l[i]))
|
||||||
display_dict = {'enum_values': l}
|
display_dict = {'enum_values': l}
|
||||||
|
elif col_type == 'text' and is_multiple:
|
||||||
|
display_dict = {'is_names': self.is_names.isChecked()}
|
||||||
|
|
||||||
if col_type in ['text', 'composite', 'enumeration']:
|
if col_type in ['text', 'composite', 'enumeration']:
|
||||||
display_dict['use_decorations'] = self.use_decorations.checkState()
|
display_dict['use_decorations'] = self.use_decorations.checkState()
|
||||||
|
@ -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,27 +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>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="is_names">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Check this box if this column contains names, like the authors column.</string>
|
||||||
|
</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>
|
||||||
@ -241,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>
|
||||||
|
@ -64,8 +64,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('tags_browser_collapse_at', gprefs)
|
r('tags_browser_collapse_at', gprefs)
|
||||||
|
|
||||||
choices = set([k for k in db.field_metadata.all_field_keys()
|
choices = set([k for k in db.field_metadata.all_field_keys()
|
||||||
if db.field_metadata[k]['is_category'] and
|
if db.field_metadata[k]['is_category'] and
|
||||||
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
|
(db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']) and
|
||||||
|
not db.field_metadata[k]['display'].get('is_names', False)])
|
||||||
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
|
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
|
||||||
choices |= set(['search'])
|
choices |= set(['search'])
|
||||||
self.opt_categories_using_hierarchy.update_items_cache(choices)
|
self.opt_categories_using_hierarchy.update_items_cache(choices)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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): # {{{
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user