Sync to trunk.

This commit is contained in:
John Schember 2011-05-06 19:23:42 -04:00
commit 3d519c3be1
166 changed files with 54631 additions and 54338 deletions

View File

@ -19,6 +19,34 @@
# new recipes:
# - title:
- version: 0.8.0
date: 2010-05-06
new features:
- title: "Go to http://calibre-ebook.com/new-in/eight to see what's new in 0.8.0"
type: major
- version: 0.7.59
date: 2011-04-30
bug fixes:
- title: "Fixes a bug in 0.7.58 that caused too small fonts when converting to MOBI for the Kindle. Apologies."
- title: "Apple driver: Handle invalid EPUBs that do not contain an OPF file"
new recipes:
- title: The Big Picture and Auto industry news
author: welovelucy
- title: Gazeta Prawna
author: Vroo
- title: Various Czech news sources
author: Tomas Latal
- title: Diario de Ibiza
author: Joan Tur
- version: 0.7.58
date: 2011-04-29

16
recipes/auto_blog.recipe Normal file
View File

@ -0,0 +1,16 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AutoBlog(BasicNewsRecipe):
title = u'Auto Blog'
__author__ = 'Welovelucy'
language = 'en'
description = 'Auto industry news'
oldest_article = 7
max_articles_per_feed = 100
feeds = [(u'AutoBlog', u'http://www.autoblog.com/rss.xml')]
def print_version(self, url):
return url + 'print/'

View File

@ -0,0 +1,12 @@
from calibre.web.feeds.news import BasicNewsRecipe
class BigPicture(BasicNewsRecipe):
title = u'The Big Picture'
__author__ = 'Welovelucy'
description = ('Macro perspective on capital markets, economy, technology'
' and digital media')
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
feeds = [(u'Big Picture', u'http://feeds.feedburner.com/TheBigPicture')]

View File

@ -3,7 +3,8 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>'
__version__ = '0.98' # 2011-04-10
__version__ = '0.98'
''' http://brandeins.de - Wirtschaftsmagazin '''
import re
import string
@ -13,8 +14,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BrandEins(BasicNewsRecipe):
title = u'brand eins'
__author__ = 'Constantin Hofstetter; Steffen Siebert'
description = u'Wirtschaftsmagazin: Gets the last full issue on default. Set a integer value for the username-field to get older issues: 1 -> the newest (but not complete) issue, 2 -> the last complete issue (default), 3 -> the issue before 2 etc.'
__author__ = 'Constantin Hofstetter'
description = u'Wirtschaftsmagazin'
publisher ='brandeins.de'
category = 'politics, business, wirtschaft, Germany'
use_embedded_content = False
@ -105,10 +106,11 @@ class BrandEins(BasicNewsRecipe):
keys = issue_map.keys()
keys.sort()
keys.reverse()
selected_issue = issue_map[keys[issue-1]]
selected_issue_key = keys[issue - 1]
selected_issue = issue_map[selected_issue_key]
url = selected_issue.get('href', False)
# Get the title for the magazin - build it out of the title of the cover - take the issue and year;
self.title = "brand eins "+ re.search(r"(?P<date>\d\d\/\d\d\d\d)", selected_issue.find('img').get('title', False)).group('date')
self.title = "brand eins " + selected_issue_key[4:] + "/" + selected_issue_key[0:4]
url = 'http://brandeins.de/'+url
# url = "http://www.brandeins.de/archiv/magazin/tierisch.html"
@ -161,3 +163,4 @@ class BrandEins(BasicNewsRecipe):
current_articles.append({'title': title, 'url': url, 'description': description, 'date':''})
titles_and_articles.append([chapter_title, current_articles])
return titles_and_articles

37
recipes/digizone.recipe Normal file
View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Tomas Latal<latal.tomas at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class DigiZoneCZ(BasicNewsRecipe):
title = 'DigiZone'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '30 April 2011'
description = u'Aktuality a \u010dl\xe1nky z DigiZone.cz'
oldest_article = 1
max_articles_per_feed = 10
encoding = 'iso-8859-2'
publisher = 'Internet Info s.r.o.'
category = 'digitalni vysilani, televize, CZ'
language = 'cs'
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
extra_css = 'p.perex{font-size: 1.2em; margin: 0 0 10px 0;line-height: 1.4;padding: 0 0 10px 0;font-weight: bold;} \
p.perex img {display:none;} \
.urs p {margin: 0 0 0.8em 0;}'
feeds = [
(u'Aktuality', u'http://rss.digizone.cz/aktuality'),
(u'\u010cl\xe1nky', u'http://rss.digizone.cz/clanky')
]
remove_tags_before = dict(id=['p-article','p-actuality'])
remove_tags_after = dict(id=['p-article','p-actuality'])
remove_tags = [
dict(attrs={'class':['path','mth','lbtr','serial','enquiry','links','dp-n','side','op-ab','op-view','op-sub','op-list',]}),
dict(id=['opinions','discussionList','similarItems','sidebar','footer','opl','promo-box'])
]

View File

@ -12,7 +12,6 @@ class AdvancedUserRecipe1301860159(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
language = 'en_EN'
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
remove_tags = [dict(name='a'),dict(name='hr')]

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
'''
foxnews.com
'''
@ -23,6 +23,7 @@ class FoxNews(BasicNewsRecipe):
extra_css = """
body{font-family: Arial,sans-serif }
.caption{font-size: x-small}
.author,.dateline{font-size: small}
"""
conversion_options = {
@ -34,12 +35,12 @@ class FoxNews(BasicNewsRecipe):
remove_attributes = ['xmlns','lang']
remove_tags = [
dict(name=['object','embed','link','script','iframe','meta','base'])
,dict(attrs={'class':['user-control','url-description','ad-context']})
remove_tags=[
dict(attrs={'class':['user-control','logo','ad-300x250','url-description']})
,dict(name=['meta','base','link','iframe','object','embed'])
]
remove_tags_before=dict(name='h1')
keep_only_tags=[dict(attrs={'id':'article-print'})]
remove_tags_after =dict(attrs={'class':'url-description'})
feeds = [
@ -55,3 +56,24 @@ class FoxNews(BasicNewsRecipe):
def print_version(self, url):
return url + 'print'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
limg = item.find('img')
if item.string is not None:
str = item.string
item.replaceWith(str)
else:
if limg:
item.name = 'div'
item.attrs = []
else:
str = self.tag_to_string(item)
item.replaceWith(str)
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2010, Tomasz Dlugosz <tomek3d@gmail.com>'
__copyright__ = u'2010-2011, Tomasz Dlugosz <tomek3d@gmail.com>'
'''
frazpc.pl
'''
@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe):
use_embedded_content = False
no_stylesheets = True
feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ]
keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})]
remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})]
preprocess_regexps = [
(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[(r'<div id="post-[0-9]*"', lambda match: '<div id="FRAZ_CONTENT"'),
(r'href="/f/news/', lambda match: 'href="http://www.frazpc.pl/f/news/'),
(r' &nbsp; <a href="http://www.frazpc.pl/[^>]*?">(Skomentuj|Komentarz(e)?\([0-9]*\))</a>&nbsp; \|', lambda match: '')]
feeds = [
(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'),
(u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly')
]
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
remove_tags = [
dict(name='div', attrs={'class':'title-wrapper'}),
dict(name='p', attrs={'class':'tags'}),
dict(name='p', attrs={'class':'article-links'}),
dict(name='div', attrs={'class':'comments_box'})
]
preprocess_regexps = [(re.compile(r'\| <a href="#comments">Komentarze \([0-9]*\)</a>'), lambda match: '')]
remove_attributes = [ 'width', 'height' ]

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Vroo <vroobelek@iq.pl>'
__author__ = u'Vroo'
'''
gazetaprawna.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class gazetaprawna(BasicNewsRecipe):
version = 1
title = u'Gazeta Prawna'
__author__ = u'Vroo'
publisher = u'Infor Biznes'
oldest_article = 7
max_articles_per_feed = 20
no_stylesheets = True
remove_javascript = True
description = 'Polski dziennik gospodarczy'
language = 'pl'
encoding = 'utf-8'
remove_tags_after = [
dict(name='div', attrs={'class':['data-art']})
]
remove_tags = [
dict(name='div', attrs={'class':['dodatki_artykulu','data-art']})
]
feeds = [
(u'Wiadomo\u015bci - najwa\u017cniejsze', u'http://www.gazetaprawna.pl/wiadomosci/najwazniejsze/rss.xml'),
(u'Biznes i prawo gospodarcze', u'http://biznes.gazetaprawna.pl/rss.xml'),
(u'Prawo i wymiar sprawiedliwo\u015bci', u'http://prawo.gazetaprawna.pl/rss.xml'),
(u'Praca i ubezpieczenia', u'http://praca.gazetaprawna.pl/rss.xml'),
(u'Podatki i rachunkowo\u015b\u0107', u'http://podatki.gazetaprawna.pl/rss.xml')
]
def print_version(self, url):
url = url.replace('wiadomosci/artykuly', 'drukowanie')
url = url.replace('artykuly', 'drukowanie')
url = url.replace('porady', 'drukowanie')
url = url.replace('wywiady', 'drukowanie')
url = url.replace('orzeczenia', 'drukowanie')
url = url.replace('galeria', 'drukowanie')
url = url.replace('komentarze', 'drukowanie')
url = url.replace('biznes.gazetaprawna', 'www.gazetaprawna')
url = url.replace('podatki.gazetaprawna', 'www.gazetaprawna')
url = url.replace('prawo.gazetaprawna', 'www.gazetaprawna')
url = url.replace('praca.gazetaprawna', 'www.gazetaprawna')
return url

View File

@ -16,7 +16,7 @@ class Jezebel(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
use_embedded_content = True
language = 'en'
masthead_url = 'http://cache.gawkerassets.com/assets/jezebel.com/img/logo.png'
extra_css = '''
@ -32,13 +32,12 @@ class Jezebel(BasicNewsRecipe):
, 'language' : language
}
remove_attributes = ['width','height']
keep_only_tags = [dict(attrs={'class':'content permalink'})]
remove_tags_before = dict(name='h1')
remove_tags = [dict(attrs={'class':'contactinfo'})]
remove_tags_after = dict(attrs={'class':'contactinfo'})
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/vip?format=xml')]
remove_tags = [
{'class': 'feedflare'},
]
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/full')]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -16,7 +16,7 @@ class Kotaku(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
use_embedded_content = True
language = 'en'
masthead_url = 'http://cache.gawkerassets.com/assets/kotaku.com/img/logo.png'
extra_css = '''
@ -31,13 +31,12 @@ class Kotaku(BasicNewsRecipe):
, 'language' : language
}
remove_attributes = ['width','height']
keep_only_tags = [dict(attrs={'class':'content permalink'})]
remove_tags_before = dict(name='h1')
remove_tags = [dict(attrs={'class':'contactinfo'})]
remove_tags_after = dict(attrs={'class':'contactinfo'})
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/vip?format=xml')]
remove_tags = [
{'class': 'feedflare'},
]
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/full')]
def preprocess_html(self, soup):
return self.adeify_images(soup)

37
recipes/lupa.recipe Normal file
View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Tomas Latal<latal.tomas at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class LupaCZ(BasicNewsRecipe):
title = 'Lupa'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '30 April 2011'
description = u'Zpr\xe1vi\u010dky a \u010dl\xe1nky z Lupa.cz'
oldest_article = 2
max_articles_per_feed = 10
encoding = 'utf8'
publisher = 'Internet Info s.r.o.'
category = 'IT,news,CZ'
language = 'cs'
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
extra_css = 'p.perex{font-size: 1.2em;margin: 0 0 10px 0;line-height: 1.4;padding: 0 0 10px 0;font-weight: bold;} \
p.perex img {display:none;} \
.urs p {margin: 0 0 0.8em 0;}'
feeds = [
(u'Zpr\xe1vi\u010dky', u'http://rss.lupa.cz/zpravicky'),
(u'\u010cl\xe1nky', u'http://rss.lupa.cz/clanky')
]
remove_tags_before = dict(id='main')
remove_tags_after = [dict(id='main')]
remove_tags = [
dict(attrs={'class':['author clear','tags-rubrics','box border style1 links clear','enquiry clear','serial','box border style1 TitleList','breadcrumb clear','article-discussion box border style1 monitoringComponentArticle','link-more border prev-next clear']}),
dict(id=['discussionList','similarItems','sidebar','footer','opl','promo-box'])
]

37
recipes/mesec.recipe Normal file
View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Tomas Latal<latal.tomas at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class MesecCZ(BasicNewsRecipe):
title = u'M\u011b\u0161ec'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '30 April 2011'
description = u'Zpr\xe1vi\u010dky a \u010dl\xe1nky z Mesec.cz'
oldest_article = 1
max_articles_per_feed = 10
encoding = 'utf8'
publisher = 'Internet Info s.r.o.'
category = 'finance,CZ'
language = 'cs'
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
extra_css = 'p.perex{font-size: 1.2em;margin: 0 0 10px 0;line-height: 1.4;padding: 0 0 10px 0;font-weight: bold;} \
p.perex img {display:none;} \
.urs p {margin: 0 0 0.8em 0;}'
feeds = [
(u'Aktuality', u'http://www.mesec.cz/rss/aktuality/'),
(u'\u010cl\xe1nky', u'http://www.mesec.cz/rss/clanky/')
]
remove_tags_before = dict(id='main')
remove_tags_after = [dict(id='main')]
remove_tags = [
dict(attrs={'class':['author clear','tags-rubrics','box border style1 links clear','enquiry clear','serial','box border style1 TitleList','breadcrumb clear','article-discussion box border style1 monitoringComponentArticle','link-more border prev-next clear']}),
dict(id=['discussionList','similarItems','sidebar','footer','opl','promo-box'])
]

View File

@ -6,8 +6,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
class NovinkyCZ(BasicNewsRecipe):
title = 'Novinky'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '24 April 2011'
__version__ = '1.1'
__date__ = '30 April 2011'
description = 'News from server Novinky.cz'
oldest_article = 1
max_articles_per_feed = 10
@ -18,6 +18,7 @@ class NovinkyCZ(BasicNewsRecipe):
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
cover_url = 'http://img193.imageshack.us/img193/3039/novinkycover.jpg'
extra_css = 'p.acmDescription{font-style:italic;} p.acmAuthor{font-size:0.8em; color:#707070}'
feeds = [

37
recipes/podnikatel.recipe Normal file
View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Tomas Latal<latal.tomas at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class PodnikatelCZ(BasicNewsRecipe):
title = 'Podnikatel'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '30 April 2011'
description = u'Aktuality a \u010dl\xe1nky z Podnikatel.cz'
oldest_article = 1
max_articles_per_feed = 10
encoding = 'utf8'
publisher = 'Internet Info s.r.o.'
category = 'podnikani, bussiness, CZ'
language = 'cs'
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
extra_css = 'p.perex{font-size: 1.2em; margin: 0 0 10px 0;line-height: 1.4;padding: 0 0 10px 0;font-weight: bold;} \
p.perex img {display:none;} \
.urs p {margin: 0 0 0.8em 0;}'
feeds = [
(u'Aktuality', u'http://rss.podnikatel.cz/aktuality'),
(u'\u010cl\xe1nky', u'http://rss.podnikatel.cz/clanky')
]
remove_tags_before = dict(id='art-content')
remove_tags_after = [dict(id='art-content')]
remove_tags = [
dict(attrs={'class':['socialshare','box-blue','author clear','labels-terms','box diskuze','ad','page-nav right','infobox','box zpravy','s-clanky']}),
dict(id=['path','article-tools','discussionList','similarItems','promo-box'])
]

View File

@ -18,7 +18,7 @@ class TelepolisNews(BasicNewsRecipe):
recursion = 0
no_stylesheets = True
encoding = "utf-8"
language = 'de_AT'
language = 'de'
use_embedded_content =False
remove_empty_feeds = True

View File

@ -10,6 +10,8 @@ import re
from calibre.web.feeds.news import BasicNewsRecipe
class Time(BasicNewsRecipe):
recipe_disabled = ('This recipe has been disabled as TIME no longer'
' publish complete articles on the web.')
title = u'Time'
__author__ = 'Kovid Goyal and Sujata Raman'
description = 'Weekly magazine'

View File

@ -7,13 +7,11 @@ usatoday.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag
import re
class USAToday(BasicNewsRecipe):
title = 'USA Today'
__author__ = 'GRiker'
__author__ = 'Kovid Goyal'
oldest_article = 1
timefmt = ''
max_articles_per_feed = 20
@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe):
margin-bottom: 0em; \
font-size: smaller;}\n \
.articleBody {text-align: left;}\n '
conversion_options = { 'linearize_tables' : True }
#simultaneous_downloads = 1
feeds = [
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe):
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
]
keep_only_tags = [dict(attrs={'class':[
'byLine',
'inside-copy',
'inside-head',
'inside-head2',
'item',
'item-block',
'photo-container',
]}),
dict(id=[
'applyMainStoryPhoto',
'permalink',
])]
remove_tags = [dict(attrs={'class':[
keep_only_tags = [dict(attrs={'class':'story'})]
remove_tags = [
dict(attrs={'class':[
'share',
'reprints',
'inline-h3',
'info-extras',
'ppy-outer',
'ppy-caption',
'comments',
'jump',
'pagetools',
'post-attributes',
'tags',
'bottom-tools',
'sponsoredlinks',
]}),
dict(id=[])]
dict(id=['pluck']),
]
#feeds = [('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles')]
def dump_hex(self, src, length=16):
''' Diagnostic '''
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
N=0; result=''
while src:
s,src = src[:length],src[length:]
hexa = ' '.join(["%02X"%ord(x) for x in s])
s = s.translate(FILTER)
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
N+=length
print result
def fixChars(self,string):
# Replace lsquo (\x91)
fixed = re.sub("\x91","&#8216;",string)
# Replace rsquo (\x92)
fixed = re.sub("\x92","&#8217;",fixed)
# Replace ldquo (\x93)
fixed = re.sub("\x93","&#8220;",fixed)
# Replace rdquo (\x94)
fixed = re.sub("\x94","&#8221;",fixed)
# Replace ndash (\x96)
fixed = re.sub("\x96","&#8211;",fixed)
# Replace mdash (\x97)
fixed = re.sub("\x97","&#8212;",fixed)
return fixed
def get_masthead_url(self):
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe):
masthead = None
return masthead
def massageNCXText(self, description):
# Kindle TOC descriptions won't render certain characters
if description:
massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
# Replace '&' with '&#38;'
massaged = re.sub("&","&#38;", massaged)
return self.fixChars(massaged)
else:
return description
def parse_feeds(self, *args, **kwargs):
parsed_feeds = BasicNewsRecipe.parse_feeds(self, *args, **kwargs)
# Count articles for progress dialog
article_count = 0
for feed in parsed_feeds:
article_count += len(feed)
self.log( "Queued %d articles" % article_count)
return parsed_feeds
def preprocess_html(self, soup):
soup = self.strip_anchors(soup)
return soup
def postprocess_html(self, soup, first_fetch):
# Remove navLinks <div class="inside-copy" style="padding-bottom:3px">
navLinks = soup.find(True,{'style':'padding-bottom:3px'})
if navLinks:
navLinks.extract()
# Remove <div class="inside-copy" style="margin-bottom:10px">
gibberish = soup.find(True,{'style':'margin-bottom:10px'})
if gibberish:
gibberish.extract()
# Change <inside-head> to <h2>
headline = soup.find(True, {'class':['inside-head','inside-head2']})
if not headline:
headline = soup.find('h3')
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, headline.contents[0])
headline.replaceWith(tag)
else:
print "unable to find headline:\n%s\n" % soup
# Change byLine to byline, change commas to middot
# Kindle renders commas in byline as '&'
byline = soup.find(True, {'class':'byLine'})
if byline:
byline['class'] = 'byline'
# Replace comma with middot
byline.contents[0].replaceWith(re.sub(","," &middot;", byline.renderContents()))
jumpout_punc_list = [':','?']
# Remove the inline jumpouts in <div class="inside-copy">
paras = soup.findAll(True, {'class':'inside-copy'})
for para in paras:
if re.match("<b>[\w\W]+ ",para.renderContents()):
p = para.find('b')
for punc in jumpout_punc_list:
punc_offset = p.contents[0].find(punc)
if punc_offset == -1:
continue
if punc_offset > 1:
if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
#print "extracting \n%s\n" % para.prettify()
para.extract()
# Reset class for remaining
paras = soup.findAll(True, {'class':'inside-copy'})
for para in paras:
para['class'] = 'articleBody'
# Remove inline jumpouts in <p>
paras = soup.findAll(['p'])
for p in paras:
if hasattr(p,'contents') and len(p.contents):
for punc in jumpout_punc_list:
punc_offset = p.contents[0].find(punc)
if punc_offset == -1:
continue
if punc_offset > 2 and hasattr(p,'a') and len(p.contents):
#print "evaluating %s\n" % p.contents[0][:punc_offset+1]
if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
#print "extracting \n%s\n" % p.prettify()
p.extract()
# Capture the first img, insert after headline
imgs = soup.findAll('img')
print "postprocess_html(): %d images" % len(imgs)
if imgs:
divTag = Tag(soup, 'div')
divTag['class'] = 'image'
body = soup.find('body')
img = imgs[0]
#print "img: \n%s\n" % img.prettify()
# Table for photo and credit
tableTag = Tag(soup,'table')
# Photo
trimgTag = Tag(soup, 'tr')
tdimgTag = Tag(soup, 'td')
tdimgTag.insert(0,img)
trimgTag.insert(0,tdimgTag)
tableTag.insert(0,trimgTag)
# Credit
trcreditTag = Tag(soup, 'tr')
tdcreditTag = Tag(soup, 'td')
tdcreditTag['class'] = 'credit'
credit = soup.find('td',{'class':'photoCredit'})
if credit:
tdcreditTag.insert(0,NavigableString(credit.renderContents()))
else:
credit = img['credit']
if credit:
tdcreditTag.insert(0,NavigableString(credit))
else:
tdcreditTag.insert(0,NavigableString(''))
trcreditTag.insert(0,tdcreditTag)
tableTag.insert(1,trcreditTag)
dtc = 0
divTag.insert(dtc,tableTag)
dtc += 1
if False:
# Add the caption in the table
tableCaptionTag = Tag(soup,'caption')
tableCaptionTag.insert(0,soup.find('td',{'class':'photoCredit'}).renderContents())
tableTag.insert(1,tableCaptionTag)
divTag.insert(dtc,tableTag)
dtc += 1
body.insert(1,divTag)
else:
# Add the caption below the table
#print "Looking for caption in this soup:\n%s" % img.prettify()
captionTag = Tag(soup,'p')
captionTag['class'] = 'caption'
if hasattr(img,'alt') and img['alt']:
captionTag.insert(0,NavigableString('<blockquote>%s</blockquote>' % img['alt']))
divTag.insert(dtc, captionTag)
dtc += 1
else:
try:
captionTag.insert(0,NavigableString('<blockquote>%s</blockquote>' % img['cutline']))
divTag.insert(dtc, captionTag)
dtc += 1
except:
pass
hrTag = Tag(soup, 'hr')
divTag.insert(dtc, hrTag)
dtc += 1
# Delete <div id="applyMainStoryPhoto"
photoJunk = soup.find('div',{'id':'applyMainStoryPhoto'})
if photoJunk:
photoJunk.extract()
# Insert img after headline
tag = body.find(True)
insertLoc = 0
headline_found = False
while True:
# Scan the top-level tags
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'headline':
headline_found = True
body.insert(insertLoc,divTag)
break
tag = tag.nextSibling
if not tag:
break
if not headline_found:
# Monolithic <div> - restructure
tag = body.find(True)
while True:
insertLoc += 1
try:
if hasattr(tag,'class') and tag['class'] == 'headline':
headline_found = True
tag.insert(insertLoc,divTag)
break
except:
pass
tag = tag.next
if not tag:
break
# Yank out headline, img and caption
headline = body.find('h2','headline')
img = body.find('div','image')
caption = body.find('p''class')
# body(0) is calibre_navbar
# body(1) is <div class="item">
btc = 1
headline.extract()
body.insert(1, headline)
btc += 1
if img:
img.extract()
body.insert(btc, img)
btc += 1
if caption:
caption.extract()
body.insert(btc, caption)
btc += 1
if len(imgs) > 1:
if True:
[img.extract() for img in imgs[1:]]
else:
# Format the remaining images
# This doesn't work yet
for img in imgs[1:]:
print "img:\n%s\n" % img.prettify()
divTag = Tag(soup, 'div')
divTag['class'] = 'image'
# Table for photo and credit
tableTag = Tag(soup,'table')
# Photo
trimgTag = Tag(soup, 'tr')
tdimgTag = Tag(soup, 'td')
tdimgTag.insert(0,img)
trimgTag.insert(0,tdimgTag)
tableTag.insert(0,trimgTag)
# Credit
trcreditTag = Tag(soup, 'tr')
tdcreditTag = Tag(soup, 'td')
tdcreditTag['class'] = 'credit'
try:
tdcreditTag.insert(0,NavigableString(img['credit']))
except:
tdcreditTag.insert(0,NavigableString(''))
trcreditTag.insert(0,tdcreditTag)
tableTag.insert(1,trcreditTag)
divTag.insert(0,tableTag)
soup.img.replaceWith(divTag)
return soup
def postprocess_book(self, oeb, opts, log) :
def extract_byline(href) :
# <meta name="byline" content=
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
byline = soup.find('div',attrs={'class':'byline'})
if byline:
byline['class'] = 'byline'
# Replace comma with middot
byline.contents[0].replaceWith(re.sub(u",", u" &middot;",
byline.renderContents(encoding=None)))
return byline.renderContents(encoding=None)
else :
paras = soup.findAll(text=True)
for para in paras:
if para.startswith("Copyright"):
return para[len('Copyright xxxx '):para.find('.')]
return None
def extract_description(href) :
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
description = soup.find('meta',attrs={'name':'description'})
if description :
return self.massageNCXText(description['content'])
else:
# Take first paragraph of article
articleBody = soup.find('div',attrs={'id':['articleBody','item']})
if articleBody:
paras = articleBody.findAll('p')
for p in paras:
if p.renderContents() > '' :
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
else:
print "Didn't find <div id='articleBody'> in this soup:\n%s" % soup.prettify()
return None
# Method entry point here
# Single section toc looks different than multi-section tocs
if oeb.toc.depth() == 2 :
for article in oeb.toc :
if article.author is None :
article.author = extract_byline(article.href)
if article.description is None :
article.description = extract_description(article.href)
elif oeb.toc.depth() == 3 :
for section in oeb.toc :
for article in section :
article.author = extract_byline(article.href)
'''
if article.author is None :
article.author = self.massageNCXText(extract_byline(article.href))
else:
article.author = self.massageNCXText(article.author)
'''
if article.description is None :
article.description = extract_description(article.href)
def strip_anchors(self,soup):
paras = soup.findAll(True)
for para in paras:
aTags = para.findAll('a')
for a in aTags:
if a.img is None:
a.replaceWith(a.renderContents().decode('cp1252','replace'))
return soup

39
recipes/vitalia.recipe Normal file
View File

@ -0,0 +1,39 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Tomas Latal<latal.tomas at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class VitaliaCZ(BasicNewsRecipe):
title = 'Vitalia'
__author__ = 'Tomas Latal'
__version__ = '1.0'
__date__ = '30 April 2011'
description = u'Aktuality a \u010dl\xe1nky z Vitalia.cz'
oldest_article = 1
max_articles_per_feed = 10
encoding = 'utf8'
publisher = 'Internet Info s.r.o.'
category = 'zdravi, vztahy, wellness, CZ'
language = 'cs'
publication_type = 'newsportal'
no_stylesheets = True
remove_javascript = True
extra_css = 'p.perex{font-size: 1.2em; margin: 0 0 10px 0; line-height: 1.4; padding: 0 0 10px 0; font-weight: bold;} \
p.perex img {display:none;} \
span.author {font-size:0.8em; font-style:italic} \
.urs div.rs-tip-major {padding:0.5em; background: #e0e0e0 none repeat scroll 0 0;border: 1px solid #909090;} \
.urs p {margin: 0 0 0.8em 0;}'
feeds = [
(u'Aktuality', 'http://www.vitalia.cz/rss/aktuality/'),
(u'\u010cl\xe1nky', u'http://www.vitalia.cz/rss/clanky/'),
]
remove_tags_before = dict(id='main')
remove_tags_after = [dict(id='main')]
remove_tags = [
dict(attrs={'class':['author clear','tags-rubrics','box border style1 links clear','enquiry clear','serial','box border style1 TitleList','breadcrumb clear','article-discussion box border style1 monitoringComponentArticle','link-more border prev-next clear']}),
dict(id=['discussionList','similarItems','sidebar','footer','opl','promo-box'])
]

View File

@ -23,6 +23,9 @@ wWinMain(HINSTANCE Inst, HINSTANCE PrevInst,
ret = execute_python_entrypoint(BASENAME, MODULE, FUNCTION,
stdout_redirect, stderr_redirect);
if (stdout != NULL) fclose(stdout);
if (stderr != NULL) fclose(stderr);
DeleteFile(stdout_redirect);
DeleteFile(stderr_redirect);

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = u'calibre'
numeric_version = (0, 7, 58)
numeric_version = (0, 8, 0)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -607,6 +607,7 @@ class StoreBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
type = _('Store')
minimum_calibre_version = (0, 8, 0)
actual_plugin = None

View File

@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
from calibre.constants import numeric_version
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.utils.config import test_eight_code
# To archive plugins {{{
class HTML2ZIP(FileTypePlugin):
@ -596,6 +595,7 @@ from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK, NOOK_COLOR
from calibre.devices.prs505.driver import PRS505
from calibre.devices.user_defined.driver import USER_DEFINED
from calibre.devices.android.driver import ANDROID, S60
from calibre.devices.nokia.driver import N770, N810, E71X, E52
from calibre.devices.eslick.driver import ESLICK, EBK52
@ -613,6 +613,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
@ -621,28 +622,16 @@ from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, TXT2TXTZ, ArchiveExtract, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
Epubcheck, ]
if test_eight_code:
# New metadata download plugins {{{
from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
from calibre.ebooks.metadata.sources.overdrive import OverDrive
from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
from calibre.ebooks.metadata.sources.overdrive import OverDrive
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive]
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive]
# }}}
else:
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
KentDistrictLibrary
from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
AmazonCovers, DoubanCovers
plugins += [GoogleBooks, ISBNDB, Amazon,
OpenLibraryCovers, AmazonCovers, DoubanCovers,
NiceBooksCovers, KentDistrictLibrary, DoubanBooks, NiceBooks]
plugins += [
ComicInput,
@ -755,6 +744,9 @@ plugins += [
EEEREADER,
NEXTBOOK,
ITUNES,
BOEYE_BEX,
BOEYE_BDX,
USER_DEFINED,
]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')]
@ -867,10 +859,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
if test_eight_code:
plugins += [ActionStore]
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
# }}}
@ -1096,10 +1085,8 @@ class Misc(PreferencesPlugin):
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
if test_eight_code:
plugins.append(MetadataSources)
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
MetadataSources]
#}}}

View File

@ -15,12 +15,11 @@ from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser, prefs
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
plugin_dir, OptionParser)
from calibre.ebooks.epub.fix import ePubFixer
from calibre.ebooks.metadata.sources.base import Source
from calibre.constants import DEBUG
builtin_names = frozenset([p.name for p in builtin_plugins])
@ -93,8 +92,7 @@ def restore_plugin_state_to_default(plugin_or_name):
config['enabled_plugins'] = ep
default_disabled_plugins = set([
'Douban Books', 'Douban.com covers', 'Nicebooks', 'Nicebooks covers',
'Kent District Library'
'Overdrive',
])
def is_disabled(plugin):
@ -190,44 +188,6 @@ def output_profiles():
yield plugin
# }}}
# Metadata sources {{{
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
for plugin in _initialized_plugins:
if isinstance(plugin, MetadataSource) and \
plugin.metadata_type == metadata_type:
if is_disabled(plugin):
continue
if customize:
customization = config['plugin_customization']
plugin.site_customization = customization.get(plugin.name, None)
if plugin.name == 'IsbnDB' and isbndb_key is not None:
plugin.site_customization = isbndb_key
yield plugin
def get_isbndb_key():
return config['plugin_customization'].get('IsbnDB', None)
def set_isbndb_key(key):
for plugin in _initialized_plugins:
if plugin.name == 'IsbnDB':
return customize_plugin(plugin, key)
def migrate_isbndb_key():
key = prefs['isbndb_com_key']
if key:
prefs.set('isbndb_com_key', '')
set_isbndb_key(key)
def cover_sources():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, CoverDownload):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Interface Actions # {{{
def interface_actions():
@ -527,7 +487,8 @@ def initialize_plugins():
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
_initialized_plugins.append(plugin)
except:
print 'Failed to initialize plugin...'
print 'Failed to initialize plugin:', repr(zfp)
if DEBUG:
traceback.print_exc()
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
reread_filetype_plugins()

View File

@ -156,3 +156,60 @@ def debug(ioreg_to_tmp=False, buf=None):
sys.stdout = oldo
sys.stderr = olde
def device_info(ioreg_to_tmp=False, buf=None):
from calibre.devices.scanner import DeviceScanner, win_pnp_drives
from calibre.constants import iswindows
import re
res = {}
device_details = {}
device_set = set()
drive_details = {}
drive_set = set()
res['device_set'] = device_set
res['device_details'] = device_details
res['drive_details'] = drive_details
res['drive_set'] = drive_set
try:
s = DeviceScanner()
s.scan()
devices = (s.devices)
if not iswindows:
devices = [list(x) for x in devices]
for dev in devices:
for i in range(3):
dev[i] = hex(dev[i])
d = dev[0] + dev[1] + dev[2]
device_set.add(d)
device_details[d] = dev[0:3]
else:
for dev in devices:
vid = re.search('vid_([0-9a-f]*)&', dev)
if vid:
vid = vid.group(1)
pid = re.search('pid_([0-9a-f]*)&', dev)
if pid:
pid = pid.group(1)
rev = re.search('rev_([0-9a-f]*)$', dev)
if rev:
rev = rev.group(1)
d = vid+pid+rev
device_set.add(d)
device_details[d] = (vid, pid, rev)
drives = win_pnp_drives(debug=False)
for drive,details in drives.iteritems():
order = 'ORD_' + str(drive.order)
ven = re.search('VEN_([^&]*)&', details)
if ven:
ven = ven.group(1)
prod = re.search('PROD_([^&]*)&', details)
if prod:
prod = prod.group(1)
d = (order, ven, prod)
drive_details[drive] = d
drive_set.add(drive)
finally:
pass
return res

View File

@ -62,7 +62,7 @@ class ANDROID(USBMS):
0x502 : { 0x3203 : [0x0100]},
# Dell
0x413c : { 0xb007 : [0x0100, 0x0224]},
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
# LG
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
@ -112,7 +112,7 @@ class ANDROID(USBMS):
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB']
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -163,6 +163,8 @@ class ITUNES(DriverBase):
settings()
set_progress_reporter()
upload_books()
_get_fpath()
_update_epub_metadata()
add_books_to_metadata()
use_plugboard_ext()
set_plugboard()
@ -504,7 +506,7 @@ class ITUNES(DriverBase):
if self.iTunes:
# Check for connected book-capable device
self.sources = self._get_sources()
if 'iPod' in self.sources:
if 'iPod' in self.sources and not self.ejected:
#if DEBUG:
#sys.stdout.write('.')
#sys.stdout.flush()
@ -2034,16 +2036,17 @@ class ITUNES(DriverBase):
if 'iPod' in self.sources:
connected_device = self.sources['iPod']
device = self.iTunes.sources[connected_device]
dev_books = None
for pl in device.playlists():
if pl.special_kind() == appscript.k.Books:
if DEBUG:
self.log.info(" Book playlist: '%s'" % (pl.name()))
books = pl.file_tracks()
dev_books = pl.file_tracks()
break
else:
self.log.error(" book_playlist not found")
for book in books:
for book in dev_books:
# This may need additional entries for international iTunes users
if book.kind() in self.Audiobooks:
if DEBUG:
@ -2621,16 +2624,13 @@ class ITUNES(DriverBase):
# Touch the OPF timestamp
try:
zf_opf = ZipFile(fpath,'r')
fnames = zf_opf.namelist()
opf = [x for x in fnames if '.opf' in x][0]
except:
raise UserFeedback("'%s' is not a valid EPUB" % metadata.title,
None,
level=UserFeedback.WARN)
fnames = zf_opf.namelist()
try:
opf = [x for x in fnames if '.opf' in x][0]
except:
opf = None
if opf:
opf_tree = etree.fromstring(zf_opf.read(opf))
md_els = opf_tree.xpath('.//*[local-name()="metadata"]')
if md_els:

View File

View File

@ -0,0 +1,56 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Ken <ken at szboeye.com>'
__docformat__ = 'restructuredtext en'
'''
Device driver for BOEYE serial readers
'''
from calibre.devices.usbms.driver import USBMS
class BOEYE_BEX(USBMS):
name = 'BOEYE BEX reader driver'
gui_name = 'BOEYE BEX'
description = _('Communicate with BOEYE BEX Serial eBook readers.')
author = 'szboeye'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
VENDOR_ID = [0x0085]
PRODUCT_ID = [0x600]
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BEX Storage Card'
EBOOK_DIR_MAIN = 'Documents'
SUPPORTS_SUB_DIRS = True
class BOEYE_BDX(USBMS):
name = 'BOEYE BDX reader driver'
gui_name = 'BOEYE BDX'
description = _('Communicate with BOEYE BDX serial eBook readers.')
author = 'szboeye'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
VENDOR_ID = [0x0085]
PRODUCT_ID = [0x800]
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
OSX_CARD_A_MEM = 'Linux File-Stor Gadget Media'
MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BDX Internal Memory'
STORAGE_CARD_VOLUME_LABEL = 'BOEYE BDX Storage Card'
EBOOK_DIR_MAIN = 'Documents'
EBOOK_DIR_CARD_A = 'Documents'
SUPPORTS_SUB_DIRS = True

View File

@ -64,7 +64,7 @@ class HANLINV3(USBMS):
return names
def linux_swap_drives(self, drives):
if len(drives) < 2: return drives
if len(drives) < 2 or not drives[1] or not drives[2]: return drives
drives = list(drives)
t = drives[0]
drives[0] = drives[1]
@ -95,7 +95,6 @@ class HANLINV5(HANLINV3):
gui_name = 'Hanlin V5'
description = _('Communicate with Hanlin V5 eBook readers.')
VENDOR_ID = [0x0492]
PRODUCT_ID = [0x8813]
BCD = [0x319]

View File

@ -164,7 +164,7 @@ class APNXBuilder(object):
if c == '/':
closing = True
continue
elif c in ('d', 'p'):
elif c == 'p':
if closing:
in_p = False
else:

View File

@ -187,7 +187,7 @@ class LUMIREAD(USBMS):
cfilepath = cfilepath.replace(os.sep+'books'+os.sep,
os.sep+'covers'+os.sep, 1)
pdir = os.path.dirname(cfilepath)
if not os.exists(pdir):
if not os.path.exists(pdir):
os.makedirs(pdir)
with open(cfilepath+'.jpg', 'wb') as f:
f.write(metadata.thumbnail[-1])

View File

@ -94,6 +94,9 @@ class DeviceConfig(object):
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
ec = []
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
if config_widget.opt_extra_customization[i] is None:
ec.append(None)
continue
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
ec.append(config_widget.opt_extra_customization[i].isChecked())
else:

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.devices.usbms.driver import USBMS
class USER_DEFINED(USBMS):
name = 'User Defined USB driver'
gui_name = 'User Defined USB Device'
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'mobi', 'pdf']
VENDOR_ID = 0xFFFF
PRODUCT_ID = 0xFFFF
BCD = None
EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD_A = ''
VENDOR_NAME = []
WINDOWS_MAIN_MEM = ''
WINDOWS_CARD_A_MEM = ''
OSX_MAIN_MEM = 'Device Main Memory'
MAIN_MEMORY_VOLUME_LABEL = 'Device Main Memory'
SUPPORTS_SUB_DIRS = True
EXTRA_CUSTOMIZATION_MESSAGE = [
_('USB Vendor ID (in hex)') + ':::<p>' +
_('Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('USB Product ID (in hex)')+ ':::<p>' +
_('Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('USB Revision ID (in hex)')+ ':::<p>' +
_('Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
'',
_('Windows main memory vendor string') + ':::<p>' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('Windows main memory ID string') + ':::<p>' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('Windows card A vendor string') + ':::<p>' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('Windows card A ID string') + ':::<p>' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
'set up the user-defined device') + '</p>',
_('Main memory folder') + ':::<p>' +
_('Enter the folder where the books are to be stored. This folder '
'is prepended to any send_to_device template') + '</p>',
_('Card A folder') + ':::<p>' +
_('Enter the folder where the books are to be stored. This folder '
'is prepended to any send_to_device template') + '</p>',
]
EXTRA_CUSTOMIZATION_DEFAULT = [
'0x0000',
'0x0000',
'0x0000',
None,
'',
'',
'',
'',
'',
'',
]
OPT_USB_VENDOR_ID = 0
OPT_USB_PRODUCT_ID = 1
OPT_USB_REVISION_ID = 2
OPT_USB_WINDOWS_MM_VEN_ID = 4
OPT_USB_WINDOWS_MM_ID = 5
OPT_USB_WINDOWS_CA_VEN_ID = 6
OPT_USB_WINDOWS_CA_ID = 7
OPT_MAIN_MEM_FOLDER = 8
OPT_CARD_A_FOLDER = 9
def initialize(self):
try:
e = self.settings().extra_customization
self.VENDOR_ID = int(e[self.OPT_USB_VENDOR_ID], 16)
self.PRODUCT_ID = int(e[self.OPT_USB_PRODUCT_ID], 16)
self.BCD = [int(e[self.OPT_USB_REVISION_ID], 16)]
if e[self.OPT_USB_WINDOWS_MM_VEN_ID]:
self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_MM_VEN_ID])
if e[self.OPT_USB_WINDOWS_CA_VEN_ID] and \
e[self.OPT_USB_WINDOWS_CA_VEN_ID] not in self.VENDOR_NAME:
self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_CA_VEN_ID])
self.WINDOWS_MAIN_MEM = e[self.OPT_USB_WINDOWS_MM_ID] + '&'
self.WINDOWS_CARD_A_MEM = e[self.OPT_USB_WINDOWS_CA_ID] + '&'
self.EBOOK_DIR_MAIN = e[self.OPT_MAIN_MEM_FOLDER]
self.EBOOK_DIR_CARD_A = e[self.OPT_CARD_A_FOLDER]
except:
import traceback
traceback.print_exc()
USBMS.initialize(self)

View File

@ -7,10 +7,12 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
import posixpath
from calibre import walk
from calibre import guess_type, walk
from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata.opf2 import OPF
from calibre.utils.zipfile import ZipFile
class HTMLZInput(InputFormatPlugin):
@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin):
# Extract content from zip archive.
zf = ZipFile(stream)
zf.extractall('.')
zf.extractall()
for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
@ -71,4 +73,23 @@ class HTMLZInput(InputFormatPlugin):
mi = get_file_type_metadata(stream, file_ext)
meta_info_to_oeb_metadata(mi, oeb.metadata, log)
# Get the cover path from the OPF.
cover_href = None
opf = None
for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.opf'):
opf = x
break
if opf:
opf = OPF(opf)
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
# Set the cover.
if cover_href:
cdata = None
with open(cover_href, 'rb') as cf:
cdata = cf.read()
id, href = oeb.manifest.generate('cover', cover_href)
oeb.manifest.add(id, href, guess_type(cover_href)[0], data=cdata)
oeb.guide.add('cover', 'Cover', href)
return oeb

View File

@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
from cStringIO import StringIO
from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile
@ -80,9 +82,30 @@ class HTMLZOutput(OutputFormatPlugin):
with open(fname, 'wb') as img:
img.write(data)
# Cover
cover_path = None
try:
cover_data = None
if oeb_book.metadata.cover:
term = oeb_book.metadata.cover[0].term
cover_data = oeb_book.guide[term].item.data
if cover_data:
from calibre.utils.magick.draw import save_cover_data_to
cover_path = os.path.join(tdir, 'cover.jpg')
with open(cover_path, 'w') as cf:
cf.write('')
save_cover_data_to(cover_data, cover_path)
except:
import traceback
traceback.print_exc()
# Metadata
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
mdataf.write(etree.tostring(oeb_book.metadata.to_opf1()))
opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1())))
mi = opf.to_book_metadata()
if cover_path:
mi.cover = 'cover.jpg'
mdataf.write(metadata_to_opf(mi))
htmlz = ZipFile(output_path, 'w')
htmlz.add_dir(tdir)

View File

@ -274,6 +274,9 @@ def check_isbn(isbn):
if not isbn:
return None
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
all_same = re.match(r'(\d)\1{9,12}$', isbn)
if all_same is not None:
return None
if len(isbn) == 10:
return check_isbn10(isbn)
if len(isbn) == 13:

View File

@ -1,224 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Fetch metadata using Amazon AWS
'''
import sys, re
from threading import RLock
from lxml import html
from lxml.html import soupparser
from calibre import browser
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.chardet import xml_to_unicode
from calibre.library.comments import sanitize_comments_html
asin_cache = {}
cover_url_cache = {}
cache_lock = RLock()
def find_asin(br, isbn):
q = 'http://www.amazon.com/s/?search-alias=aps&field-keywords='+isbn
res = br.open_novisit(q)
raw = res.read()
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
root = html.fromstring(raw)
revs = root.xpath('//*[@class="asinReviewsSummary" and @name]')
revs = [x.get('name') for x in revs]
if revs:
return revs[0]
def to_asin(br, isbn):
with cache_lock:
ans = asin_cache.get(isbn, None)
if ans:
return ans
if ans is False:
return None
if len(isbn) == 13:
try:
asin = find_asin(br, isbn)
except:
import traceback
traceback.print_exc()
asin = None
else:
asin = isbn
with cache_lock:
asin_cache[isbn] = asin if asin else False
return asin
def get_social_metadata(title, authors, publisher, isbn):
mi = Metadata(title, authors)
if not isbn:
return mi
isbn = check_isbn(isbn)
if not isbn:
return mi
br = browser()
asin = to_asin(br, isbn)
if asin and get_metadata(br, asin, mi):
return mi
from calibre.ebooks.metadata.xisbn import xisbn
for i in xisbn.get_associated_isbns(isbn):
asin = to_asin(br, i)
if asin and get_metadata(br, asin, mi):
return mi
return mi
def get_cover_url(isbn, br):
isbn = check_isbn(isbn)
if not isbn:
return None
with cache_lock:
ans = cover_url_cache.get(isbn, None)
if ans:
return ans
if ans is False:
return None
asin = to_asin(br, isbn)
if asin:
ans = _get_cover_url(br, asin)
if ans:
with cache_lock:
cover_url_cache[isbn] = ans
return ans
from calibre.ebooks.metadata.xisbn import xisbn
for i in xisbn.get_associated_isbns(isbn):
asin = to_asin(br, i)
if asin:
ans = _get_cover_url(br, asin)
if ans:
with cache_lock:
cover_url_cache[isbn] = ans
cover_url_cache[i] = ans
return ans
with cache_lock:
cover_url_cache[isbn] = False
return None
def _get_cover_url(br, asin):
q = 'http://amzn.com/'+asin
try:
raw = br.open_novisit(q).read()
except Exception as e:
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return None
raise
if '<title>404 - ' in raw:
return None
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
root = soupparser.fromstring(raw)
except:
return False
imgs = root.xpath('//img[@id="prodImage" and @src]')
if imgs:
src = imgs[0].get('src')
parts = src.split('/')
if len(parts) > 3:
bn = parts[-1]
sparts = bn.split('_')
if len(sparts) > 2:
bn = sparts[0] + sparts[-1]
return ('/'.join(parts[:-1]))+'/'+bn
return None
def get_metadata(br, asin, mi):
q = 'http://amzn.com/'+asin
try:
raw = br.open_novisit(q).read()
except Exception as e:
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return False
raise
if '<title>404 - ' in raw:
return False
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
root = soupparser.fromstring(raw)
except:
return False
if root.xpath('//*[@id="errorMessage"]'):
return False
ratings = root.xpath('//div[@class="jumpBar"]/descendant::span[@class="asinReviewsSummary"]')
pat = re.compile(r'([0-9.]+) out of (\d+) stars')
if ratings:
for elem in ratings[0].xpath('descendant::*[@title]'):
t = elem.get('title').strip()
m = pat.match(t)
if m is not None:
try:
mi.rating = float(m.group(1))/float(m.group(2)) * 5
except:
pass
desc = root.xpath('//div[@id="productDescription"]/*[@class="content"]')
if desc:
desc = desc[0]
for c in desc.xpath('descendant::*[@class="seeAll" or'
' @class="emptyClear" or @href]'):
c.getparent().remove(c)
desc = html.tostring(desc, method='html', encoding=unicode).strip()
# remove all attributes from tags
desc = re.sub(r'<([a-zA-Z0-9]+)\s[^>]+>', r'<\1>', desc)
# Collapse whitespace
#desc = re.sub('\n+', '\n', desc)
#desc = re.sub(' +', ' ', desc)
# Remove the notice about text referring to out of print editions
desc = re.sub(r'(?s)<em>--This text ref.*?</em>', '', desc)
# Remove comments
desc = re.sub(r'(?s)<!--.*?-->', '', desc)
mi.comments = sanitize_comments_html(desc)
return True
def main(args=sys.argv):
import tempfile, os
tdir = tempfile.gettempdir()
br = browser()
for title, isbn in [
('The Heroes', '9780316044981'), # Test find_asin
('Learning Python', '8324616489'), # Test xisbn
('Angels & Demons', '9781416580829'), # Test sophisticated comment formatting
# Random tests
('Star Trek: Destiny: Mere Mortals', '9781416551720'),
('The Great Gatsby', '0743273567'),
]:
cpath = os.path.join(tdir, title+'.jpg')
curl = get_cover_url(isbn, br)
if curl is None:
print 'No cover found for', title
else:
open(cpath, 'wb').write(br.open_novisit(curl).read())
print 'Cover for', title, 'saved to', cpath
#import time
#st = time.time()
mi = get_social_metadata(title, None, None, isbn)
if not mi.comments:
print 'Failed to downlaod social metadata for', title
return 1
#print '\n\n', time.time() - st, '\n\n'
print mi
print '\n'
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,516 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2010, sengian <sengian1@gmail.com>'
import sys, textwrap, re, traceback
from urllib import urlencode
from math import ceil
from lxml import html
from lxml.html import soupparser
from calibre.utils.date import parse_date, utcnow, replace_months
from calibre.utils.cleantext import clean_ascii_chars
from calibre import browser, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import MetaInformation, check_isbn, \
authors_to_sort_string
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import OptionParser
from calibre.library.comments import sanitize_comments_html
class AmazonFr(MetadataSource):
name = 'Amazon French'
description = _('Downloads metadata from amazon.fr')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
has_html_comments = True
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='fr')
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
class AmazonEs(MetadataSource):
name = 'Amazon Spanish'
description = _('Downloads metadata from amazon.com in spanish')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
has_html_comments = True
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='es')
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
class AmazonEn(MetadataSource):
name = 'Amazon English'
description = _('Downloads metadata from amazon.com in english')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
has_html_comments = True
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='en')
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
class AmazonDe(MetadataSource):
name = 'Amazon German'
description = _('Downloads metadata from amazon.de')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
has_html_comments = True
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='de')
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
class Amazon(MetadataSource):
name = 'Amazon'
description = _('Downloads metadata from amazon.com')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal & Sengian'
version = (1, 1, 0)
has_html_comments = True
def fetch(self):
# if not self.site_customization:
# return
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose, lang='all')
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
# @property
# def string_customization_help(self):
# return _('You can select here the language for metadata search with amazon.com')
def report(verbose):
if verbose:
traceback.print_exc()
class Query(object):
BASE_URL_ALL = 'http://www.amazon.com'
BASE_URL_FR = 'http://www.amazon.fr'
BASE_URL_DE = 'http://www.amazon.de'
def __init__(self, title=None, author=None, publisher=None, isbn=None, keywords=None,
max_results=20, rlang='all'):
assert not(title is None and author is None and publisher is None \
and isbn is None and keywords is None)
assert (max_results < 21)
self.max_results = int(max_results)
self.renbres = re.compile(u'\s*(\d+)\s*')
q = { 'search-alias' : 'stripbooks' ,
'unfiltered' : '1',
'field-keywords' : '',
'field-author' : '',
'field-title' : '',
'field-isbn' : '',
'field-publisher' : ''
#get to amazon detailed search page to get all options
# 'node' : '',
# 'field-binding' : '',
#before, during, after
# 'field-dateop' : '',
#month as number
# 'field-datemod' : '',
# 'field-dateyear' : '',
#french only
# 'field-collection' : '',
#many options available
}
if rlang =='all':
q['sort'] = 'relevanceexprank'
self.urldata = self.BASE_URL_ALL
elif rlang =='es':
q['sort'] = 'relevanceexprank'
q['field-language'] = 'Spanish'
self.urldata = self.BASE_URL_ALL
elif rlang =='en':
q['sort'] = 'relevanceexprank'
q['field-language'] = 'English'
self.urldata = self.BASE_URL_ALL
elif rlang =='fr':
q['sort'] = 'relevancerank'
self.urldata = self.BASE_URL_FR
elif rlang =='de':
q['sort'] = 'relevancerank'
self.urldata = self.BASE_URL_DE
self.baseurl = self.urldata
if isbn is not None:
q['field-isbn'] = isbn.replace('-', '')
else:
if title is not None:
q['field-title'] = title
if author is not None:
q['field-author'] = author
if publisher is not None:
q['field-publisher'] = publisher
if keywords is not None:
q['field-keywords'] = keywords
if isinstance(q, unicode):
q = q.encode('utf-8')
self.urldata += '/gp/search/ref=sr_adv_b/?' + urlencode(q)
def __call__(self, browser, verbose, timeout = 5.):
if verbose:
print 'Query:', self.urldata
try:
raw = browser.open_novisit(self.urldata, timeout=timeout).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
raise
if '<title>404 - ' in raw:
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
return soupparser.fromstring(clean_ascii_chars(raw))
except:
return None, self.urldata
#nb of page
try:
nbresults = self.renbres.findall(feed.xpath("//*[@class='resultCount']")[0].text)
except:
return None, self.urldata
pages =[feed]
if len(nbresults) > 1:
nbpagetoquery = int(ceil(float(min(int(nbresults[2]), self.max_results))/ int(nbresults[1])))
for i in xrange(2, nbpagetoquery + 1):
try:
urldata = self.urldata + '&page=' + str(i)
raw = browser.open_novisit(urldata, timeout=timeout).read()
except Exception as e:
continue
if '<title>404 - ' in raw:
continue
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
return soupparser.fromstring(clean_ascii_chars(raw))
except:
continue
pages.append(feed)
results = []
for x in pages:
results.extend([i.getparent().get('href') \
for i in x.xpath("//a/span[@class='srTitle']")])
return results[:self.max_results], self.baseurl
class ResultList(list):
def __init__(self, baseurl, lang = 'all'):
self.baseurl = baseurl
self.lang = lang
self.repub = re.compile(u'\((.*)\)')
self.rerat = re.compile(u'([0-9.]+)')
self.reattr = re.compile(r'<([a-zA-Z0-9]+)\s[^>]+>')
self.reoutp = re.compile(r'(?s)<em>--This text ref.*?</em>')
self.recom = re.compile(r'(?s)<!--.*?-->')
self.republi = re.compile(u'(Editeur|Publisher|Verlag)', re.I)
self.reisbn = re.compile(u'(ISBN-10|ISBN-10|ASIN)', re.I)
self.relang = re.compile(u'(Language|Langue|Sprache)', re.I)
self.reratelt = re.compile(u'(Average\s*Customer\s*Review|Moyenne\s*des\s*commentaires\s*client|Durchschnittliche\s*Kundenbewertung)', re.I)
self.reprod = re.compile(u'(Product\s*Details|D.tails\s*sur\s*le\s*produit|Produktinformation)', re.I)
def strip_tags_etree(self, etreeobj, invalid_tags):
for (itag, rmv) in invalid_tags.iteritems():
if rmv:
for elts in etreeobj.getiterator(itag):
elts.drop_tree()
else:
for elts in etreeobj.getiterator(itag):
elts.drop_tag()
def clean_entry(self, entry, invalid_tags = {'script': True},
invalid_id = (), invalid_class=()):
#invalid_tags: remove tag and keep content if False else remove
#remove tags
if invalid_tags:
self.strip_tags_etree(entry, invalid_tags)
#remove id
if invalid_id:
for eltid in invalid_id:
elt = entry.get_element_by_id(eltid)
if elt is not None:
elt.drop_tree()
#remove class
if invalid_class:
for eltclass in invalid_class:
elts = entry.find_class(eltclass)
if elts is not None:
for elt in elts:
elt.drop_tree()
def get_title(self, entry):
title = entry.get_element_by_id('btAsinTitle')
if title is not None:
title = title.text
return unicode(title.replace('\n', '').strip())
def get_authors(self, entry):
author = entry.get_element_by_id('btAsinTitle')
while author.getparent().tag != 'div':
author = author.getparent()
author = author.getparent()
authortext = []
for x in author.getiterator('a'):
authortext.append(unicode(x.text_content().strip()))
return authortext
def get_description(self, entry, verbose):
try:
description = entry.get_element_by_id("productDescription").find("div[@class='content']")
inv_class = ('seeAll', 'emptyClear')
inv_tags ={'img': True, 'a': False}
self.clean_entry(description, invalid_tags=inv_tags, invalid_class=inv_class)
description = html.tostring(description, method='html', encoding=unicode).strip()
# remove all attributes from tags
description = self.reattr.sub(r'<\1>', description)
# Remove the notice about text referring to out of print editions
description = self.reoutp.sub('', description)
# Remove comments
description = self.recom.sub('', description)
return unicode(sanitize_comments_html(description))
except:
report(verbose)
return None
def get_tags(self, entry, browser, verbose):
try:
tags = entry.get_element_by_id('tagContentHolder')
testptag = tags.find_class('see-all')
if testptag:
for x in testptag:
alink = x.xpath('descendant-or-self::a')
if alink:
if alink[0].get('class') == 'tgJsActive':
continue
link = self.baseurl + alink[0].get('href')
entry = self.get_individual_metadata(browser, link, verbose)
tags = entry.get_element_by_id('tagContentHolder')
break
tags = [a.text for a in tags.getiterator('a') if a.get('rel') == 'tag']
except:
report(verbose)
tags = []
return tags
def get_book_info(self, entry, mi, verbose):
try:
entry = entry.get_element_by_id('SalesRank').getparent()
except:
try:
for z in entry.getiterator('h2'):
if self.reprod.search(z.text_content()):
entry = z.getparent().find("div[@class='content']/ul")
break
except:
report(verbose)
return mi
elts = entry.findall('li')
#pub & date
elt = filter(lambda x: self.republi.search(x.find('b').text), elts)
if elt:
pub = elt[0].find('b').tail
mi.publisher = unicode(self.repub.sub('', pub).strip())
d = self.repub.search(pub)
if d is not None:
d = d.group(1)
try:
default = utcnow().replace(day=15)
if self.lang != 'all':
d = replace_months(d, self.lang)
d = parse_date(d, assume_utc=True, default=default)
mi.pubdate = d
except:
report(verbose)
#ISBN
elt = filter(lambda x: self.reisbn.search(x.find('b').text), elts)
if elt:
isbn = elt[0].find('b').tail.replace('-', '').strip()
if check_isbn(isbn):
mi.isbn = unicode(isbn)
elif len(elt) > 1:
isbn = elt[1].find('b').tail.replace('-', '').strip()
if check_isbn(isbn):
mi.isbn = unicode(isbn)
#Langue
elt = filter(lambda x: self.relang.search(x.find('b').text), elts)
if elt:
langue = elt[0].find('b').tail.strip()
if langue:
mi.language = unicode(langue)
#ratings
elt = filter(lambda x: self.reratelt.search(x.find('b').text), elts)
if elt:
ratings = elt[0].find_class('swSprite')
if ratings:
ratings = self.rerat.findall(ratings[0].get('title'))
if len(ratings) == 2:
mi.rating = float(ratings[0])/float(ratings[1]) * 5
return mi
def fill_MI(self, entry, title, authors, browser, verbose):
mi = MetaInformation(title, authors)
mi.author_sort = authors_to_sort_string(authors)
mi.comments = self.get_description(entry, verbose)
mi = self.get_book_info(entry, mi, verbose)
mi.tags = self.get_tags(entry, browser, verbose)
return mi
def get_individual_metadata(self, browser, linkdata, verbose):
try:
raw = browser.open_novisit(linkdata).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
raise
if '<title>404 - ' in raw:
report(verbose)
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
return soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
return soupparser.fromstring(clean_ascii_chars(raw))
except:
report(verbose)
return
def populate(self, entries, browser, verbose=False):
for x in entries:
try:
entry = self.get_individual_metadata(browser, x, verbose)
# clean results
# inv_ids = ('divsinglecolumnminwidth', 'sims.purchase', 'AutoBuyXGetY', 'A9AdsMiddleBoxTop')
# inv_class = ('buyingDetailsGrid', 'productImageGrid')
# inv_tags ={'script': True, 'style': True, 'form': False}
# self.clean_entry(entry, invalid_id=inv_ids)
title = self.get_title(entry)
authors = self.get_authors(entry)
except Exception as e:
if verbose:
print 'Failed to get all details for an entry'
print e
print 'URL who failed:', x
report(verbose)
continue
self.append(self.fill_MI(entry, title, authors, browser, verbose))
def search(title=None, author=None, publisher=None, isbn=None,
max_results=5, verbose=False, keywords=None, lang='all'):
br = browser()
entries, baseurl = Query(title=title, author=author, isbn=isbn, publisher=publisher,
keywords=keywords, max_results=max_results,rlang=lang)(br, verbose)
if entries is None or len(entries) == 0:
return
#List of entry
ans = ResultList(baseurl, lang)
ans.populate(entries, br, verbose)
return ans
def option_parser():
parser = OptionParser(textwrap.dedent(\
_('''\
%prog [options]
Fetch book metadata from Amazon. You must specify one of title, author,
ISBN, publisher or keywords. Will fetch a maximum of 10 matches,
so you should make your query as specific as possible.
You can chose the language for metadata retrieval:
All & english & french & german & spanish
'''
)))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-k', '--keywords', help='Keywords')
parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch')
parser.add_option('-l', '--lang', default='all',
help='Chosen language for metadata search (all, en, fr, es, de)')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, isbn=opts.isbn, publisher=opts.publisher,
keywords=opts.keywords, verbose=opts.verbose, max_results=opts.max_results,
lang=opts.lang)
except AssertionError:
report(True)
parser.print_help()
return 1
if results is None or len(results) == 0:
print 'No result found for this search!'
return 0
for result in results:
print unicode(result).encode(preferred_encoding, 'replace')
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,317 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import traceback, socket, sys
from functools import partial
from threading import Thread, Event
from Queue import Queue, Empty
from lxml import etree
import mechanize
from calibre.customize import Plugin
from calibre import browser, prints
from calibre.constants import preferred_encoding, DEBUG
class CoverDownload(Plugin):
'''
These plugins are used to download covers for books.
'''
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('Cover download')
def has_cover(self, mi, ans, timeout=5.):
'''
Check if the book described by mi has a cover. Call ans.set() if it
does. Do nothing if it doesn't.
:param mi: MetaInformation object
:param timeout: timeout in seconds
:param ans: A threading.Event object
'''
raise NotImplementedError()
def get_covers(self, mi, result_queue, abort, timeout=5.):
'''
Download covers for books described by the mi object. Downloaded covers
must be put into the result_queue. If more than one cover is available,
the plugin should continue downloading them and putting them into
result_queue until abort.is_set() returns True.
:param mi: MetaInformation object
:param result_queue: A multithreaded Queue
:param abort: A threading.Event object
:param timeout: timeout in seconds
'''
raise NotImplementedError()
def exception_to_string(self, ex):
try:
return unicode(ex)
except:
try:
return str(ex).decode(preferred_encoding, 'replace')
except:
return repr(ex)
def debug(self, *args, **kwargs):
if DEBUG:
prints('\t'+self.name+':', *args, **kwargs)
class HeadRequest(mechanize.Request):
def get_method(self):
return 'HEAD'
class OpenLibraryCovers(CoverDownload): # {{{
'Download covers from openlibrary.org'
# See http://openlibrary.org/dev/docs/api/covers
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
name = 'openlibrary.org covers'
description = _('Download covers from openlibrary.org')
author = 'Kovid Goyal'
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
from calibre.ebooks.metadata.library_thing import get_browser
br = get_browser()
br.set_handle_redirect(False)
try:
br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 302:
self.debug('cover for', mi.isbn, 'found')
ans.set()
else:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
from calibre.ebooks.metadata.library_thing import get_browser
br = get_browser()
try:
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
result_queue.put((True, ans, 'jpg', self.name))
except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
result_queue.put((False, _('ISBN: %s not found')%mi.isbn, '', self.name))
else:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
class AmazonCovers(CoverDownload): # {{{
name = 'amazon.com covers'
description = _('Download covers from amazon.com')
author = 'Kovid Goyal'
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
from calibre.ebooks.metadata.amazon import get_cover_url
br = browser()
try:
get_cover_url(mi.isbn, br)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception as e:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
from calibre.ebooks.metadata.amazon import get_cover_url
br = browser()
try:
url = get_cover_url(mi.isbn, br)
if url is None:
raise ValueError('No cover found for ISBN: %s'%mi.isbn)
cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name))
except Exception as e:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
def check_for_cover(mi, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
ans = Event()
checkers = [partial(p.has_cover, mi, ans, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in checkers]
for w in workers:
w.daemon = True
w.start()
while not ans.is_set():
ans.wait(0.1)
if sum([int(w.is_alive()) for w in workers]) == 0:
break
return ans.is_set()
# }}}
def download_covers(mi, result_queue, max_covers=50, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
abort = Event()
temp = Queue()
getters = [partial(p.get_covers, mi, temp, abort, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in getters]
for w in workers:
w.daemon = True
w.start()
count = 0
while count < max_covers:
try:
result = temp.get_nowait()
if result[0]:
count += 1
result_queue.put(result)
except Empty:
pass
if sum([int(w.is_alive()) for w in workers]) == 0:
break
abort.set()
while True:
try:
result = temp.get_nowait()
count += 1
result_queue.put(result)
except Empty:
break
# }}}
class DoubanCovers(CoverDownload): # {{{
'Download covers from Douban.com'
DOUBAN_ISBN_URL = 'http://api.douban.com/book/subject/isbn/'
CALIBRE_DOUBAN_API_KEY = '0bd1672394eb1ebf2374356abec15c3d'
name = 'Douban.com covers'
description = _('Download covers from Douban.com')
author = 'Li Fanxi'
def get_cover_url(self, isbn, br, timeout=5.):
try:
url = self.DOUBAN_ISBN_URL + isbn + "?apikey=" + self.CALIBRE_DOUBAN_API_KEY
src = br.open(url, timeout=timeout).read()
except Exception as err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
err = Exception(_('Douban.com API timed out. Try again later.'))
raise err
else:
feed = etree.fromstring(src)
NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom',
'db': 'http://www.douban.com/xmlns/'
}
XPath = partial(etree.XPath, namespaces=NAMESPACES)
entries = XPath('//atom:entry')(feed)
if len(entries) < 1:
return None
try:
cover_url = XPath("descendant::atom:link[@rel='image']/attribute::href")
u = cover_url(entries[0])[0].replace('/spic/', '/lpic/');
# If URL contains "book-default", the book doesn't have a cover
if u.find('book-default') != -1:
return None
except:
return None
return u
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
try:
if self.get_cover_url(mi.isbn, br, timeout=timeout) != None:
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception as e:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name))
except Exception as e:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
def download_cover(mi, timeout=5.): # {{{
results = Queue()
download_covers(mi, results, max_covers=1, timeout=timeout)
errors, ans = [], None
while True:
try:
x = results.get_nowait()
if x[0]:
ans = x[1]
else:
errors.append(x)
except Empty:
break
return ans, errors
# }}}
def test(isbns): # {{{
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation('test', ['test'])
for isbn in isbns:
prints('Testing ISBN:', isbn)
mi.isbn = isbn
found = check_for_cover(mi)
prints('Has cover:', found)
ans, errors = download_cover(mi)
if ans is not None:
prints('Cover downloaded')
else:
prints('Download failed:')
for err in errors:
prints('\t', err[-1]+':', err[1])
print '\n'
# }}}
if __name__ == '__main__':
isbns = sys.argv[1:] + ['9781591025412', '9780307272119']
#test(isbns)
from calibre.ebooks.metadata import MetaInformation
oc = OpenLibraryCovers(None)
for isbn in isbns:
mi = MetaInformation('xx', ['yy'])
mi.isbn = isbn
rq = Queue()
oc.get_covers(mi, rq, Event())
result = rq.get_nowait()
if not result[0]:
print 'Failed for ISBN:', isbn
print result

View File

@ -1,263 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>; 2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
import traceback
from urllib import urlencode
from functools import partial
from lxml import etree
from calibre import browser, preferred_encoding
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.date import parse_date, utcnow
NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom',
'db': 'http://www.douban.com/xmlns/'
}
XPath = partial(etree.XPath, namespaces=NAMESPACES)
total_results = XPath('//openSearch:totalResults')
start_index = XPath('//openSearch:startIndex')
items_per_page = XPath('//openSearch:itemsPerPage')
entry = XPath('//atom:entry')
entry_id = XPath('descendant::atom:id')
title = XPath('descendant::atom:title')
description = XPath('descendant::atom:summary')
publisher = XPath("descendant::db:attribute[@name='publisher']")
isbn = XPath("descendant::db:attribute[@name='isbn13']")
date = XPath("descendant::db:attribute[@name='pubdate']")
creator = XPath("descendant::db:attribute[@name='author']")
tag = XPath("descendant::db:tag")
CALIBRE_DOUBAN_API_KEY = '0bd1672394eb1ebf2374356abec15c3d'
class DoubanBooks(MetadataSource):
name = 'Douban Books'
description = _('Downloads metadata from Douban.com')
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
version = (1, 0, 1) # The version number of this plugin
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10,
verbose=self.verbose)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
def report(verbose):
if verbose:
import traceback
traceback.print_exc()
class Query(object):
SEARCH_URL = 'http://api.douban.com/book/subjects?'
ISBN_URL = 'http://api.douban.com/book/subject/isbn/'
type = "search"
def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, start_index=1, api_key=''):
assert not(title is None and author is None and publisher is None and \
isbn is None)
assert (int(max_results) < 21)
q = ''
if isbn is not None:
q = isbn
self.type = 'isbn'
else:
def build_term(parts):
return ' '.join(x for x in parts)
if title is not None:
q += build_term(title.split())
if author is not None:
q += (' ' if q else '') + build_term(author.split())
if publisher is not None:
q += (' ' if q else '') + build_term(publisher.split())
self.type = 'search'
if isinstance(q, unicode):
q = q.encode('utf-8')
if self.type == "isbn":
self.url = self.ISBN_URL + q
if api_key != '':
self.url = self.url + "?apikey=" + api_key
else:
self.url = self.SEARCH_URL+urlencode({
'q':q,
'max-results':max_results,
'start-index':start_index,
})
if api_key != '':
self.url = self.url + "&apikey=" + api_key
def __call__(self, browser, verbose):
if verbose:
print 'Query:', self.url
if self.type == "search":
feed = etree.fromstring(browser.open(self.url).read())
total = int(total_results(feed)[0].text)
start = int(start_index(feed)[0].text)
entries = entry(feed)
new_start = start + len(entries)
if new_start > total:
new_start = 0
return entries, new_start
elif self.type == "isbn":
feed = etree.fromstring(browser.open(self.url).read())
entries = entry(feed)
return entries, 0
class ResultList(list):
def get_description(self, entry, verbose):
try:
desc = description(entry)
if desc:
return 'SUMMARY:\n'+desc[0].text
except:
report(verbose)
def get_title(self, entry):
candidates = [x.text for x in title(entry)]
return ': '.join(candidates)
def get_authors(self, entry):
m = creator(entry)
if not m:
m = []
m = [x.text for x in m]
return m
def get_tags(self, entry, verbose):
try:
btags = [x.attrib["name"] for x in tag(entry)]
tags = []
for t in btags:
tags.extend([y.strip() for y in t.split('/')])
tags = list(sorted(list(set(tags))))
except:
report(verbose)
tags = []
return [x.replace(',', ';') for x in tags]
def get_publisher(self, entry, verbose):
try:
pub = publisher(entry)[0].text
except:
pub = None
return pub
def get_isbn(self, entry, verbose):
try:
isbn13 = isbn(entry)[0].text
except Exception:
isbn13 = None
return isbn13
def get_date(self, entry, verbose):
try:
d = date(entry)
if d:
default = utcnow().replace(day=15)
d = parse_date(d[0].text, assume_utc=True, default=default)
else:
d = None
except:
report(verbose)
d = None
return d
def populate(self, entries, browser, verbose=False, api_key=''):
for x in entries:
try:
id_url = entry_id(x)[0].text
title = self.get_title(x)
except:
report(verbose)
mi = MetaInformation(title, self.get_authors(x))
try:
if api_key != '':
id_url = id_url + "?apikey=" + api_key
raw = browser.open(id_url).read()
feed = etree.fromstring(raw)
x = entry(feed)[0]
except Exception as e:
if verbose:
print 'Failed to get all details for an entry'
print e
mi.comments = self.get_description(x, verbose)
mi.tags = self.get_tags(x, verbose)
mi.isbn = self.get_isbn(x, verbose)
mi.publisher = self.get_publisher(x, verbose)
mi.pubdate = self.get_date(x, verbose)
self.append(mi)
def search(title=None, author=None, publisher=None, isbn=None,
verbose=False, max_results=40, api_key=None):
br = browser()
start, entries = 1, []
if api_key is None:
api_key = CALIBRE_DOUBAN_API_KEY
while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, max_results=max_results, start_index=start, api_key=api_key)(br, verbose)
if not new:
break
entries.extend(new)
entries = entries[:max_results]
ans = ResultList()
ans.populate(entries, br, verbose, api_key)
return ans
def option_parser():
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from Douban. You must specify one of title, author,
publisher or ISBN. If you specify ISBN the others are ignored. Will
fetch a maximum of 100 matches, so you should make your query as
specific as possible.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, opts.publisher, opts.isbn,
verbose=opts.verbose, max_results=int(opts.max_results))
except AssertionError:
report(True)
parser.print_help()
return 1
for result in results:
print unicode(result).encode(preferred_encoding)
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -13,7 +13,7 @@ import posixpath
from cStringIO import StringIO
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZipFile, safe_replace
@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True):
opf = OPF(opf_stream)
mi = opf.to_book_metadata()
if extract_cover:
cover_name = opf.raster_cover
if cover_name:
mi.cover_data = ('jpg', zf.read(cover_name))
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
if cover_href:
mi.cover_data = ('jpg', zf.read(cover_href))
except:
return mi
return mi
@ -59,17 +59,20 @@ def set_metadata(stream, mi):
except:
pass
if new_cdata:
raster_cover = opf.raster_cover
if not raster_cover:
raster_cover = 'cover.jpg'
cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover)
cover = opf.cover
if not cover:
cover = 'cover.jpg'
cpath = posixpath.join(posixpath.dirname(opf_path), cover)
new_cover = _write_new_cover(new_cdata, cpath)
replacements[cpath] = open(new_cover.name, 'rb')
mi.cover = cover
# Update the metadata.
opf.smart_update(mi, replace_metadata=True)
old_mi = opf.to_book_metadata()
old_mi.smart_update(mi)
opf.smart_update(metadata_to_opf(old_mi), replace_metadata=True)
newopf = StringIO(opf.render())
safe_replace(stream, opf_path, newopf, extra_replacements=replacements)
safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True)
# Cleanup temporary files.
try:

View File

@ -1,523 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import traceback, sys, textwrap, re
from threading import Thread
from calibre import prints
from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log
from calibre.utils.titlecase import titlecase
from calibre.customize import Plugin
from calibre.ebooks.metadata.covers import check_for_cover
from calibre.utils.html2text import html2text
metadata_config = None
class MetadataSource(Plugin): # {{{
'''
Represents a source to query for metadata. Subclasses must implement
at least the fetch method.
When :meth:`fetch` is called, the `self` object will have the following
useful attributes (each of which may be None)::
title, book_author, publisher, isbn, log, verbose and extra
Use these attributes to construct the search query. extra is reserved for
future use.
The fetch method must store the results in `self.results` as a list of
:class:`Metadata` objects. If there is an error, it should be stored
in `self.exception` and `self.tb` (for the traceback).
'''
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
#: The type of metadata fetched. 'basic' means basic metadata like
#: title/author/isbn/etc. 'social' means social metadata like
#: tags/rating/reviews/etc.
metadata_type = 'basic'
#: If not None, the customization dialog will allow for string
#: based customization as well the default customization. The
#: string customization will be saved in the site_customization
#: member.
string_customization_help = None
#: Set this to true if your plugin returns HTML markup in comments.
#: Then if the user disables HTML, calibre will automagically convert
#: the HTML to Markdown.
has_html_comments = False
type = _('Metadata download')
def __call__(self, title, author, publisher, isbn, verbose, log=None,
extra=None):
self.worker = Thread(target=self._fetch)
self.worker.daemon = True
self.title = title
self.verbose = verbose
self.book_author = author
self.publisher = publisher
self.isbn = isbn
self.log = log if log is not None else default_log
self.extra = extra
self.exception, self.tb, self.results = None, None, []
self.worker.start()
def _fetch(self):
try:
self.fetch()
if self.results:
c = self.config_store().get(self.name, {})
res = self.results
if hasattr(res, 'authors'):
res = [res]
for mi in res:
if not c.get('rating', True):
mi.rating = None
if not c.get('comments', True):
mi.comments = None
if not c.get('tags', True):
mi.tags = []
if self.has_html_comments and mi.comments and \
c.get('textcomments', False):
try:
mi.comments = html2text(mi.comments)
except:
traceback.print_exc()
mi.comments = None
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
def fetch(self):
'''
All the actual work is done here.
'''
raise NotImplementedError
def join(self):
return self.worker.join()
def is_alive(self):
return self.worker.is_alive()
def is_customizable(self):
return True
def config_store(self):
global metadata_config
if metadata_config is None:
from calibre.utils.config import XMLConfig
metadata_config = XMLConfig('plugins/metadata_download')
return metadata_config
def config_widget(self):
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, Qt, QLineEdit, \
QCheckBox
from calibre.customize.ui import config
w = QWidget()
w._layout = QVBoxLayout(w)
w.setLayout(w._layout)
if self.string_customization_help is not None:
w._sc_label = QLabel(self.string_customization_help, w)
w._layout.addWidget(w._sc_label)
customization = config['plugin_customization']
def_sc = customization.get(self.name, '')
if not def_sc:
def_sc = ''
w._sc = QLineEdit(def_sc, w)
w._layout.addWidget(w._sc)
w._sc_label.setWordWrap(True)
w._sc_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse
| Qt.LinksAccessibleByKeyboard)
w._sc_label.setOpenExternalLinks(True)
c = self.config_store()
c = c.get(self.name, {})
for x, l in {'rating':_('ratings'), 'tags':_('tags'),
'comments':_('description/reviews')}.items():
cb = QCheckBox(_('Download %s from %s')%(l,
self.name))
setattr(w, '_'+x, cb)
cb.setChecked(c.get(x, True))
w._layout.addWidget(cb)
if self.has_html_comments:
cb = QCheckBox(_('Convert comments downloaded from %s to plain text')%(self.name))
setattr(w, '_textcomments', cb)
cb.setChecked(c.get('textcomments', False))
w._layout.addWidget(cb)
return w
def save_settings(self, w):
dl_settings = {}
for x in ('rating', 'tags', 'comments'):
dl_settings[x] = getattr(w, '_'+x).isChecked()
if self.has_html_comments:
dl_settings['textcomments'] = getattr(w, '_textcomments').isChecked()
c = self.config_store()
c.set(self.name, dl_settings)
if hasattr(w, '_sc'):
sc = unicode(w._sc.text()).strip()
from calibre.customize.ui import customize_plugin
customize_plugin(self, sc)
def customization_help(self):
return 'This plugin can only be customized using the GUI'
# }}}
class GoogleBooks(MetadataSource): # {{{
name = 'Google Books'
description = _('Downloads metadata from Google Books')
def fetch(self):
from calibre.ebooks.metadata.google_books import search
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10,
verbose=self.verbose)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
# }}}
class ISBNDB(MetadataSource): # {{{
name = 'IsbnDB'
description = _('Downloads metadata from isbndb.com')
def fetch(self):
if not self.site_customization:
return
from calibre.ebooks.metadata.isbndb import option_parser, create_books
args = ['isbndb']
if self.isbn:
args.extend(['--isbn', self.isbn])
else:
if self.title:
args.extend(['--title', self.title])
if self.book_author:
args.extend(['--author', self.book_author])
if self.publisher:
args.extend(['--publisher', self.publisher])
if self.verbose:
args.extend(['--verbose'])
args.append(self.site_customization) # IsbnDb key
try:
opts, args = option_parser().parse_args(args)
self.results = create_books(opts, args)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
@property
def string_customization_help(self):
ans = _('To use isbndb.com you must sign up for a %sfree account%s '
'and enter your access key below.')
return '<p>'+ans%('<a href="http://www.isbndb.com">', '</a>')
# }}}
class Amazon(MetadataSource): # {{{
name = 'Amazon'
metadata_type = 'social'
description = _('Downloads social metadata from amazon.com')
has_html_comments = True
def fetch(self):
if not self.isbn:
return
from calibre.ebooks.metadata.amazon import get_social_metadata
try:
self.results = get_social_metadata(self.title, self.book_author,
self.publisher, self.isbn)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
# }}}
class KentDistrictLibrary(MetadataSource): # {{{
name = 'Kent District Library'
metadata_type = 'social'
description = _('Downloads series information from ww2.kdl.org. '
'This website cannot handle large numbers of queries, '
'so the plugin is disabled by default.')
def fetch(self):
if not self.title or not self.book_author:
return
from calibre.ebooks.metadata.kdl import get_series
try:
self.results = get_series(self.title, self.book_author)
except Exception as e:
import traceback
traceback.print_exc()
self.exception = e
self.tb = traceback.format_exc()
# }}}
def result_index(source, result):
if not result.isbn:
return -1
for i, x in enumerate(source):
if x.isbn == result.isbn:
return i
return -1
def merge_results(one, two):
if two is not None and one is not None:
for x in two:
idx = result_index(one, x)
if idx < 0:
one.append(x)
else:
one[idx].smart_update(x)
class MetadataSources(object):
def __init__(self, sources):
self.sources = sources
def __enter__(self):
for s in self.sources:
s.__enter__()
return self
def __exit__(self, *args):
for s in self.sources:
s.__exit__()
def __call__(self, *args, **kwargs):
for s in self.sources:
s(*args, **kwargs)
def join(self):
for s in self.sources:
s.join()
def filter_metadata_results(item):
keywords = ["audio", "tape", "cassette", "abridged", "playaway"]
for keyword in keywords:
if item.publisher and keyword in item.publisher.lower():
return False
return True
def do_cover_check(item):
item.has_cover = False
try:
item.has_cover = check_for_cover(item)
except:
pass # Cover not found
def check_for_covers(items):
threads = [Thread(target=do_cover_check, args=(item,)) for item in items]
for t in threads: t.start()
for t in threads: t.join()
def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
verbose=0):
assert not(title is None and author is None and publisher is None and \
isbn is None)
from calibre.customize.ui import metadata_sources, migrate_isbndb_key
migrate_isbndb_key()
if isbn is not None:
isbn = re.sub(r'[^a-zA-Z0-9]', '', isbn).upper()
fetchers = list(metadata_sources(isbndb_key=isbndb_key))
with MetadataSources(fetchers) as manager:
manager(title, author, publisher, isbn, verbose)
manager.join()
results = list(fetchers[0].results) if fetchers else []
for fetcher in fetchers[1:]:
merge_results(results, fetcher.results)
results = list(filter(filter_metadata_results, results))
check_for_covers(results)
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 sort_func(x, y):
def cleanup_title(s):
if s is None:
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()
t = cleanup_title(title)
x_title = cleanup_title(x.title)
y_title = cleanup_title(y.title)
# prefer titles that start with the search title
tx = cmp(t, x_title)
ty = cmp(t, y_title)
result = 0 if abs(tx) == abs(ty) else abs(tx) - abs(ty)
# then prefer titles that have a cover image
if result == 0:
result = -cmp(x.has_cover, y.has_cover)
# then prefer titles with the longest comment, with in 10%
if result == 0:
cx = len(x.comments.strip() if x.comments else '')
cy = len(y.comments.strip() if y.comments else '')
t = (cx + cy) / 20
result = cy - cx
if abs(result) < t:
result = 0
return result
results = sorted(results, cmp=sort_func)
# if for some reason there is no comment in the top selection, go looking for one
if len(results) > 1:
if not results[0].comments or len(results[0].comments) == 0:
for r in results[1:]:
try:
if title and title.lower() == r.title[:len(title)].lower() \
and r.comments and len(r.comments):
results[0].comments = r.comments
break
except:
pass
# Find a pubdate
pubdate = None
for r in results:
if r.pubdate is not None:
pubdate = r.pubdate
break
if pubdate is not None:
for r in results:
if r.pubdate is None:
r.pubdate = pubdate
def fix_case(x):
if x:
x = titlecase(x)
return x
for r in results:
r.title = fix_case(r.title)
if r.authors:
r.authors = list(map(fix_case, r.authors))
return results, [(x.name, x.exception, x.tb) for x in fetchers]
def get_social_metadata(mi, verbose=0):
from calibre.customize.ui import metadata_sources
fetchers = list(metadata_sources(metadata_type='social'))
with MetadataSources(fetchers) as manager:
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
manager.join()
ratings, tags, comments, series, series_index = [], set([]), set([]), None, None
for fetcher in fetchers:
if fetcher.results:
dmi = fetcher.results
if dmi.rating is not None:
ratings.append(dmi.rating)
if dmi.tags:
for t in dmi.tags:
tags.add(t)
if mi.pubdate is None and dmi.pubdate is not None:
mi.pubdate = dmi.pubdate
if dmi.comments:
comments.add(dmi.comments)
if dmi.series is not None:
series = dmi.series
if dmi.series_index is not None:
series_index = dmi.series_index
if ratings:
rating = sum(ratings)/float(len(ratings))
if mi.rating is None or mi.rating < 0.1:
mi.rating = rating
else:
mi.rating = (mi.rating + rating)/2.0
if tags:
if not mi.tags:
mi.tags = []
mi.tags += list(tags)
mi.tags = list(sorted(list(set(mi.tags))))
if comments:
if not mi.comments or len(mi.comments)+20 < len(' '.join(comments)):
mi.comments = ''
for x in comments:
mi.comments += x+'\n\n'
if series and series_index is not None:
mi.series = series
mi.series_index = series_index
return [(x.name, x.exception, x.tb) for x in fetchers if x.exception is not
None]
def option_parser():
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from online sources. You must specify at least one
of title, author, publisher or ISBN. If you specify ISBN, the others
are ignored.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch')
parser.add_option('-k', '--isbndb-key',
help=('The access key for your ISBNDB.com account. '
'Only needed if you want to search isbndb.com '
'and you haven\'t customized the IsbnDB plugin.'))
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
results, exceptions = search(opts.title, opts.author, opts.publisher,
opts.isbn, opts.isbndb_key, opts.verbose)
social_exceptions = []
for result in results:
social_exceptions.extend(get_social_metadata(result, opts.verbose))
prints(unicode(result))
print
for name, exception, tb in exceptions+social_exceptions:
if exception is not None:
print 'WARNING: Fetching from', name, 'failed with error:'
print exception
print tb
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,390 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2010, sengian <sengian1@gmail.com>'
__docformat__ = 'restructuredtext en'
import sys, textwrap, re, traceback, socket
from urllib import urlencode
from lxml.html import soupparser, tostring
from calibre import browser, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import MetaInformation, check_isbn, \
authors_to_sort_string
from calibre.library.comments import sanitize_comments_html
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import OptionParser
from calibre.utils.date import parse_date, utcnow
from calibre.utils.cleantext import clean_ascii_chars
class Fictionwise(MetadataSource): # {{{
author = 'Sengian'
name = 'Fictionwise'
description = _('Downloads metadata from Fictionwise')
has_html_comments = True
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
# }}}
class FictionwiseError(Exception):
pass
def report(verbose):
if verbose:
traceback.print_exc()
class Query(object):
BASE_URL = 'http://www.fictionwise.com/servlet/mw'
def __init__(self, title=None, author=None, publisher=None, keywords=None, max_results=20):
assert not(title is None and author is None and publisher is None and keywords is None)
assert (max_results < 21)
self.max_results = int(max_results)
q = { 'template' : 'searchresults_adv.htm' ,
'searchtitle' : '',
'searchauthor' : '',
'searchpublisher' : '',
'searchkeyword' : '',
#possibilities startoflast, fullname, lastfirst
'searchauthortype' : 'startoflast',
'searchcategory' : '',
'searchcategory2' : '',
'searchprice_s' : '0',
'searchprice_e' : 'ANY',
'searchformat' : '',
'searchgeo' : 'US',
'searchfwdatetype' : '',
#maybe use dates fields if needed?
#'sortorder' : 'DESC',
#many options available: b.SortTitle, a.SortName,
#b.DateFirstPublished, b.FWPublishDate
'sortby' : 'b.SortTitle'
}
if title is not None:
q['searchtitle'] = title
if author is not None:
q['searchauthor'] = author
if publisher is not None:
q['searchpublisher'] = publisher
if keywords is not None:
q['searchkeyword'] = keywords
if isinstance(q, unicode):
q = q.encode('utf-8')
self.urldata = urlencode(q)
def __call__(self, browser, verbose, timeout = 5.):
if verbose:
print _('Query: %s') % self.BASE_URL+self.urldata
try:
raw = browser.open_novisit(self.BASE_URL, self.urldata, timeout=timeout).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
if isinstance(getattr(e, 'args', [None])[0], socket.timeout):
raise FictionwiseError(_('Fictionwise timed out. Try again later.'))
raise FictionwiseError(_('Fictionwise encountered an error.'))
if '<title>404 - ' in raw:
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
feed = soupparser.fromstring(clean_ascii_chars(raw))
except:
return None
# get list of results as links
results = feed.xpath("//table[3]/tr/td[2]/table/tr/td/p/table[2]/tr[@valign]")
results = results[:self.max_results]
results = [i.xpath('descendant-or-self::a')[0].get('href') for i in results]
#return feed if no links ie normally a single book or nothing
if not results:
results = [feed]
return results
class ResultList(list):
BASE_URL = 'http://www.fictionwise.com'
COLOR_VALUES = {'BLUE': 4, 'GREEN': 3, 'YELLOW': 2, 'RED': 1, 'NA': 0}
def __init__(self):
self.retitle = re.compile(r'\[[^\[\]]+\]')
self.rechkauth = re.compile(r'.*book\s*by', re.I)
self.redesc = re.compile(r'book\s*description\s*:\s*(<br[^>]+>)*(?P<desc>.*)<br[^>]*>.{,15}publisher\s*:', re.I)
self.repub = re.compile(r'.*publisher\s*:\s*', re.I)
self.redate = re.compile(r'.*release\s*date\s*:\s*', re.I)
self.retag = re.compile(r'.*book\s*category\s*:\s*', re.I)
self.resplitbr = re.compile(r'<br[^>]*>', re.I)
self.recomment = re.compile(r'(?s)<!--.*?-->')
self.reimg = re.compile(r'<img[^>]*>', re.I)
self.resanitize = re.compile(r'\[HTML_REMOVED\]\s*', re.I)
self.renbcom = re.compile('(?P<nbcom>\d+)\s*Reader Ratings:')
self.recolor = re.compile('(?P<ncolor>[^/]+).gif')
self.resplitbrdiv = re.compile(r'(<br[^>]+>|</?div[^>]*>)', re.I)
self.reisbn = re.compile(r'.*ISBN\s*:\s*', re.I)
def strip_tags_etree(self, etreeobj, invalid_tags):
for (itag, rmv) in invalid_tags.iteritems():
if rmv:
for elts in etreeobj.getiterator(itag):
elts.drop_tree()
else:
for elts in etreeobj.getiterator(itag):
elts.drop_tag()
def clean_entry(self, entry, invalid_tags = {'script': True},
invalid_id = (), invalid_class=(), invalid_xpath = ()):
#invalid_tags: remove tag and keep content if False else remove
#remove tags
if invalid_tags:
self.strip_tags_etree(entry, invalid_tags)
#remove xpath
if invalid_xpath:
for eltid in invalid_xpath:
elt = entry.xpath(eltid)
for el in elt:
el.drop_tree()
#remove id
if invalid_id:
for eltid in invalid_id:
elt = entry.get_element_by_id(eltid)
if elt is not None:
elt.drop_tree()
#remove class
if invalid_class:
for eltclass in invalid_class:
elts = entry.find_class(eltclass)
if elts is not None:
for elt in elts:
elt.drop_tree()
def output_entry(self, entry, prettyout = True, htmlrm="\d+"):
out = tostring(entry, pretty_print=prettyout)
#try to work around tostring to remove this encoding for exemle
reclean = re.compile('(\n+|\t+|\r+|&#'+htmlrm+';)')
return reclean.sub('', out)
def get_title(self, entry):
title = entry.findtext('./')
return self.retitle.sub('', title).strip()
def get_authors(self, entry):
authortext = entry.find('./br').tail
if not self.rechkauth.search(authortext):
return []
authortext = self.rechkauth.sub('', authortext)
return [a.strip() for a in authortext.split('&')]
def get_rating(self, entrytable, verbose):
nbcomment = tostring(entrytable.getprevious())
try:
nbcomment = self.renbcom.search(nbcomment).group("nbcom")
except:
report(verbose)
return None
hval = dict((self.COLOR_VALUES[self.recolor.search(image.get('src', default='NA.gif')).group("ncolor")],
float(image.get('height', default=0))) \
for image in entrytable.getiterator('img'))
#ratings as x/5
return float(1.25*sum(k*v for (k, v) in hval.iteritems())/sum(hval.itervalues()))
def get_description(self, entry):
description = self.output_entry(entry.xpath('./p')[1],htmlrm="")
description = self.redesc.search(description)
if not description or not description.group("desc"):
return None
#remove invalid tags
description = self.reimg.sub('', description.group("desc"))
description = self.recomment.sub('', description)
description = self.resanitize.sub('', sanitize_comments_html(description))
return _('SUMMARY:\n %s') % re.sub(r'\n\s+</p>','\n</p>', description)
def get_publisher(self, entry):
publisher = self.output_entry(entry.xpath('./p')[1])
publisher = filter(lambda x: self.repub.search(x) is not None,
self.resplitbr.split(publisher))
if not len(publisher):
return None
publisher = self.repub.sub('', publisher[0])
return publisher.split(',')[0].strip()
def get_tags(self, entry):
tag = self.output_entry(entry.xpath('./p')[1])
tag = filter(lambda x: self.retag.search(x) is not None,
self.resplitbr.split(tag))
if not len(tag):
return []
return map(lambda x: x.strip(), self.retag.sub('', tag[0]).split('/'))
def get_date(self, entry, verbose):
date = self.output_entry(entry.xpath('./p')[1])
date = filter(lambda x: self.redate.search(x) is not None,
self.resplitbr.split(date))
if not len(date):
return None
try:
d = self.redate.sub('', date[0])
if d:
default = utcnow().replace(day=15)
d = parse_date(d, assume_utc=True, default=default)
else:
d = None
except:
report(verbose)
d = None
return d
def get_ISBN(self, entry):
isbns = self.output_entry(entry.xpath('./p')[2])
isbns = filter(lambda x: self.reisbn.search(x) is not None,
self.resplitbrdiv.split(isbns))
if not len(isbns):
return None
isbns = [self.reisbn.sub('', x) for x in isbns if check_isbn(self.reisbn.sub('', x))]
return sorted(isbns, cmp=lambda x,y:cmp(len(x), len(y)))[-1]
def fill_MI(self, entry, title, authors, ratings, verbose):
mi = MetaInformation(title, authors)
mi.rating = ratings
mi.comments = self.get_description(entry)
mi.publisher = self.get_publisher(entry)
mi.tags = self.get_tags(entry)
mi.pubdate = self.get_date(entry, verbose)
mi.isbn = self.get_ISBN(entry)
mi.author_sort = authors_to_sort_string(authors)
return mi
def get_individual_metadata(self, browser, linkdata, verbose):
try:
raw = browser.open_novisit(self.BASE_URL + linkdata).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
if isinstance(getattr(e, 'args', [None])[0], socket.timeout):
raise FictionwiseError(_('Fictionwise timed out. Try again later.'))
raise FictionwiseError(_('Fictionwise encountered an error.'))
if '<title>404 - ' in raw:
report(verbose)
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
return soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
return soupparser.fromstring(clean_ascii_chars(raw))
except:
return None
def populate(self, entries, browser, verbose=False):
inv_tags ={'script': True, 'a': False, 'font': False, 'strong': False, 'b': False,
'ul': False, 'span': False}
inv_xpath =('./table',)
#single entry
if len(entries) == 1 and not isinstance(entries[0], str):
try:
entry = entries.xpath("//table[3]/tr/td[2]/table[1]/tr/td/font/table/tr/td")
self.clean_entry(entry, invalid_tags=inv_tags, invalid_xpath=inv_xpath)
title = self.get_title(entry)
#maybe strenghten the search
ratings = self.get_rating(entry.xpath("./p/table")[1], verbose)
authors = self.get_authors(entry)
except Exception as e:
if verbose:
print _('Failed to get all details for an entry')
print e
return
self.append(self.fill_MI(entry, title, authors, ratings, verbose))
else:
#multiple entries
for x in entries:
try:
entry = self.get_individual_metadata(browser, x, verbose)
entry = entry.xpath("//table[3]/tr/td[2]/table[1]/tr/td/font/table/tr/td")[0]
self.clean_entry(entry, invalid_tags=inv_tags, invalid_xpath=inv_xpath)
title = self.get_title(entry)
#maybe strenghten the search
ratings = self.get_rating(entry.xpath("./p/table")[1], verbose)
authors = self.get_authors(entry)
except Exception as e:
if verbose:
print _('Failed to get all details for an entry')
print e
continue
self.append(self.fill_MI(entry, title, authors, ratings, verbose))
def search(title=None, author=None, publisher=None, isbn=None,
min_viewability='none', verbose=False, max_results=5,
keywords=None):
br = browser()
entries = Query(title=title, author=author, publisher=publisher,
keywords=keywords, max_results=max_results)(br, verbose, timeout = 15.)
#List of entry
ans = ResultList()
ans.populate(entries, br, verbose)
return ans
def option_parser():
parser = OptionParser(textwrap.dedent(\
_('''\
%prog [options]
Fetch book metadata from Fictionwise. You must specify one of title, author,
or keywords. No ISBN specification possible. Will fetch a maximum of 20 matches,
so you should make your query as specific as possible.
''')
))
parser.add_option('-t', '--title', help=_('Book title'))
parser.add_option('-a', '--author', help=_('Book author(s)'))
parser.add_option('-p', '--publisher', help=_('Book publisher'))
parser.add_option('-k', '--keywords', help=_('Keywords'))
parser.add_option('-m', '--max-results', default=20,
help=_('Maximum number of results to fetch'))
parser.add_option('-v', '--verbose', default=0, action='count',
help=_('Be more verbose about errors'))
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, publisher=opts.publisher,
keywords=opts.keywords, verbose=opts.verbose, max_results=opts.max_results)
except AssertionError:
report(True)
parser.print_help()
return 1
if results is None or len(results) == 0:
print _('No result found for this search!')
return 0
for result in results:
print unicode(result).encode(preferred_encoding, 'replace')
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,247 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
from urllib import urlencode
from functools import partial
from lxml import etree
from calibre import browser, preferred_encoding
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import OptionParser
from calibre.utils.date import parse_date, utcnow
NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom',
'dc': 'http://purl.org/dc/terms'
}
XPath = partial(etree.XPath, namespaces=NAMESPACES)
total_results = XPath('//openSearch:totalResults')
start_index = XPath('//openSearch:startIndex')
items_per_page = XPath('//openSearch:itemsPerPage')
entry = XPath('//atom:entry')
entry_id = XPath('descendant::atom:id')
creator = XPath('descendant::dc:creator')
identifier = XPath('descendant::dc:identifier')
title = XPath('descendant::dc:title')
date = XPath('descendant::dc:date')
publisher = XPath('descendant::dc:publisher')
subject = XPath('descendant::dc:subject')
description = XPath('descendant::dc:description')
language = XPath('descendant::dc:language')
def report(verbose):
if verbose:
import traceback
traceback.print_exc()
class Query(object):
BASE_URL = 'http://books.google.com/books/feeds/volumes?'
def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, min_viewability='none', start_index=1):
assert not(title is None and author is None and publisher is None and \
isbn is None)
assert (max_results < 21)
assert (min_viewability in ('none', 'partial', 'full'))
q = ''
if isbn is not None:
q += 'isbn:'+isbn
else:
def build_term(prefix, parts):
return ' '.join('in'+prefix + ':' + x for x in parts)
if title is not None:
q += build_term('title', title.split())
if author is not None:
q += ('+' if q else '')+build_term('author', author.split())
if publisher is not None:
q += ('+' if q else '')+build_term('publisher', publisher.split())
if isinstance(q, unicode):
q = q.encode('utf-8')
self.url = self.BASE_URL+urlencode({
'q':q,
'max-results':max_results,
'start-index':start_index,
'min-viewability':min_viewability,
})
def __call__(self, browser, verbose):
if verbose:
print 'Query:', self.url
feed = etree.fromstring(browser.open(self.url).read())
#print etree.tostring(feed, pretty_print=True)
total = int(total_results(feed)[0].text)
start = int(start_index(feed)[0].text)
entries = entry(feed)
new_start = start + len(entries)
if new_start > total:
new_start = 0
return entries, new_start
class ResultList(list):
def get_description(self, entry, verbose):
try:
desc = description(entry)
if desc:
return 'SUMMARY:\n'+desc[0].text
except:
report(verbose)
def get_language(self, entry, verbose):
try:
l = language(entry)
if l:
return l[0].text
except:
report(verbose)
def get_title(self, entry):
candidates = [x.text for x in title(entry)]
return ': '.join(candidates)
def get_authors(self, entry):
m = creator(entry)
if not m:
m = []
m = [x.text for x in m]
return m
def get_author_sort(self, entry, verbose):
for x in creator(entry):
for key, val in x.attrib.items():
if key.endswith('file-as'):
return val
def get_identifiers(self, entry, mi):
isbns = []
for x in identifier(entry):
t = str(x.text).strip()
if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):
if t[:5].upper() == 'ISBN:':
isbns.append(t[5:])
if isbns:
mi.isbn = sorted(isbns, cmp=lambda x,y:cmp(len(x), len(y)))[-1]
def get_tags(self, entry, verbose):
try:
btags = [x.text for x in subject(entry)]
tags = []
for t in btags:
tags.extend([y.strip() for y in t.split('/')])
tags = list(sorted(list(set(tags))))
except:
report(verbose)
tags = []
return [x.replace(',', ';') for x in tags]
def get_publisher(self, entry, verbose):
try:
pub = publisher(entry)[0].text
except:
pub = None
return pub
def get_date(self, entry, verbose):
try:
d = date(entry)
if d:
default = utcnow().replace(day=15)
d = parse_date(d[0].text, assume_utc=True, default=default)
else:
d = None
except:
report(verbose)
d = None
return d
def populate(self, entries, browser, verbose=False):
for x in entries:
try:
id_url = entry_id(x)[0].text
title = self.get_title(x)
except:
report(verbose)
mi = MetaInformation(title, self.get_authors(x))
try:
raw = browser.open(id_url).read()
feed = etree.fromstring(raw)
x = entry(feed)[0]
except Exception as e:
if verbose:
print 'Failed to get all details for an entry'
print e
mi.author_sort = self.get_author_sort(x, verbose)
mi.comments = self.get_description(x, verbose)
self.get_identifiers(x, mi)
mi.tags = self.get_tags(x, verbose)
mi.publisher = self.get_publisher(x, verbose)
mi.pubdate = self.get_date(x, verbose)
mi.language = self.get_language(x, verbose)
self.append(mi)
def search(title=None, author=None, publisher=None, isbn=None,
min_viewability='none', verbose=False, max_results=40):
br = browser()
br.set_handle_gzip(True)
start, entries = 1, []
while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, min_viewability=min_viewability)(br, verbose)
if not new:
break
entries.extend(new)
entries = entries[:max_results]
ans = ResultList()
ans.populate(entries, br, verbose)
return ans
def option_parser():
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from Google. You must specify one of title, author,
publisher or ISBN. If you specify ISBN the others are ignored. Will
fetch a maximum of 100 matches, so you should make your query as
specific as possible.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, opts.publisher, opts.isbn,
verbose=opts.verbose, max_results=opts.max_results)
except AssertionError:
report(True)
parser.print_help()
return 1
for result in results:
print unicode(result).encode(preferred_encoding)
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,159 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Interface to isbndb.com. My key HLLXQX2A.
'''
import sys, re
from urllib import quote
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre import browser
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&'
class ISBNDBError(Exception):
pass
def fetch_metadata(url, max=3, timeout=5.):
books = []
page_number = 1
total_results = 31
br = browser()
while len(books) < total_results and max > 0:
try:
raw = br.open(url, timeout=timeout).read()
except Exception as err:
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
soup = BeautifulStoneSoup(raw,
convertEntities=BeautifulStoneSoup.XML_ENTITIES)
book_list = soup.find('booklist')
if book_list is None:
errmsg = soup.find('errormessage').string
raise ISBNDBError('Error fetching metadata: '+errmsg)
total_results = int(book_list['total_results'])
page_number += 1
np = '&page_number=%s&'%page_number
url = re.sub(r'\&page_number=\d+\&', np, url)
books.extend(book_list.findAll('bookdata'))
max -= 1
return books
class ISBNDBMetadata(Metadata):
def __init__(self, book):
Metadata.__init__(self, None)
def tostring(e):
if not hasattr(e, 'string'):
return None
ans = e.string
if ans is not None:
ans = unicode(ans).strip()
if not ans:
ans = None
return ans
self.isbn = unicode(book.get('isbn13', book.get('isbn')))
title = tostring(book.find('titlelong'))
if not title:
title = tostring(book.find('title'))
self.title = title
self.title = unicode(self.title).strip()
authors = []
au = tostring(book.find('authorstext'))
if au:
au = au.strip()
temp = au.split(',')
for au in temp:
if not au: continue
authors.extend([a.strip() for a in au.split('&amp;')])
if authors:
self.authors = authors
try:
self.author_sort = tostring(book.find('authors').find('person'))
if self.authors and self.author_sort == self.authors[0]:
self.author_sort = None
except:
pass
self.publisher = tostring(book.find('publishertext'))
summ = tostring(book.find('summary'))
if summ:
self.comments = 'SUMMARY:\n'+summ
def build_isbn(base_url, opts):
return base_url + 'index1=isbn&value1='+opts.isbn
def build_combined(base_url, opts):
query = ' '.join([e for e in (opts.title, opts.author, opts.publisher) \
if e is not None ])
query = query.strip()
if len(query) == 0:
raise ISBNDBError('You must specify at least one of --author, --title or --publisher')
query = re.sub(r'\s+', '+', query)
if isinstance(query, unicode):
query = query.encode('utf-8')
return base_url+'index1=combined&value1='+quote(query, '+')
def option_parser():
parser = OptionParser(usage=\
_('''
%prog [options] key
Fetch metadata for books from isndb.com. You can specify either the
books ISBN ID or its title and author. If you specify the title and author,
then more than one book may be returned.
key is the account key you generate after signing up for a free account from isbndb.com.
'''))
parser.add_option('-i', '--isbn', default=None, dest='isbn',
help=_('The ISBN ID of the book you want metadata for.'))
parser.add_option('-a', '--author', dest='author',
default=None, help=_('The author whose book to search for.'))
parser.add_option('-t', '--title', dest='title',
default=None, help=_('The title of the book to search for.'))
parser.add_option('-p', '--publisher', default=None, dest='publisher',
help=_('The publisher of the book to search for.'))
parser.add_option('-v', '--verbose', default=False,
action='store_true', help=_('Verbose processing'))
return parser
def create_books(opts, args, timeout=5.):
base_url = BASE_URL%dict(key=args[1])
if opts.isbn is not None:
url = build_isbn(base_url, opts)
else:
url = build_combined(base_url, opts)
if opts.verbose:
print ('ISBNDB query: '+url)
tans = [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)]
#remove duplicates ISBN
return list(dict((book.isbn, book) for book in tans).values())
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) != 2:
parser.print_help()
print ('You must supply the isbndb.com key')
return 1
for book in create_books(opts, args):
print unicode(book).encode('utf-8')
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,411 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2010, sengian <sengian1@gmail.com>'
__docformat__ = 'restructuredtext en'
import sys, textwrap, re, traceback, socket
from urllib import urlencode
from math import ceil
from copy import deepcopy
from lxml.html import soupparser
from calibre.utils.date import parse_date, utcnow, replace_months
from calibre.utils.cleantext import clean_ascii_chars
from calibre import browser, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import MetaInformation, check_isbn, \
authors_to_sort_string
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.ebooks.metadata.covers import CoverDownload
from calibre.utils.config import OptionParser
class NiceBooks(MetadataSource):
name = 'Nicebooks'
description = _('Downloads metadata from french Nicebooks')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10, verbose=self.verbose)
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
class NiceBooksCovers(CoverDownload):
name = 'Nicebooks covers'
description = _('Downloads covers from french Nicebooks')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
type = _('Cover download')
version = (1, 0, 0)
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
try:
entry = Query(isbn=mi.isbn, max_results=1)(br, False, timeout)[0]
if Covers(mi.isbn)(entry).check_cover():
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception as e:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
entry = Query(isbn=mi.isbn, max_results=1)(br, False, timeout)[0]
cover_data, ext = Covers(mi.isbn)(entry).get_cover(br, timeout)
if not ext:
ext = 'jpg'
result_queue.put((True, cover_data, ext, self.name))
except Exception as e:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
class NiceBooksError(Exception):
pass
class ISBNNotFound(NiceBooksError):
pass
def report(verbose):
if verbose:
traceback.print_exc()
class Query(object):
BASE_URL = 'http://fr.nicebooks.com/'
def __init__(self, title=None, author=None, publisher=None, isbn=None, keywords=None, max_results=20):
assert not(title is None and author is None and publisher is None \
and isbn is None and keywords is None)
assert (max_results < 21)
self.max_results = int(max_results)
if isbn is not None:
q = isbn
else:
q = ' '.join([i for i in (title, author, publisher, keywords) \
if i is not None])
if isinstance(q, unicode):
q = q.encode('utf-8')
self.urldata = 'search?' + urlencode({'q':q,'s':'Rechercher'})
def __call__(self, browser, verbose, timeout = 5.):
if verbose:
print _('Query: %s') % self.BASE_URL+self.urldata
try:
raw = browser.open_novisit(self.BASE_URL+self.urldata, timeout=timeout).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
if isinstance(getattr(e, 'args', [None])[0], socket.timeout):
raise NiceBooksError(_('Nicebooks timed out. Try again later.'))
raise NiceBooksError(_('Nicebooks encountered an error.'))
if '<title>404 - ' in raw:
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
feed = soupparser.fromstring(clean_ascii_chars(raw))
except:
return None
#nb of page to call
try:
nbresults = int(feed.xpath("//div[@id='topbar']/b")[0].text)
except:
#direct hit
return [feed]
nbpagetoquery = int(ceil(float(min(nbresults, self.max_results))/10))
pages =[feed]
if nbpagetoquery > 1:
for i in xrange(2, nbpagetoquery + 1):
try:
urldata = self.urldata + '&p=' + str(i)
raw = browser.open_novisit(self.BASE_URL+urldata, timeout=timeout).read()
except Exception as e:
continue
if '<title>404 - ' in raw:
continue
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
feed = soupparser.fromstring(clean_ascii_chars(raw))
except:
continue
pages.append(feed)
results = []
for x in pages:
results.extend([i.find_class('title')[0].get('href') \
for i in x.xpath("//ul[@id='results']/li")])
return results[:self.max_results]
class ResultList(list):
BASE_URL = 'http://fr.nicebooks.com'
def __init__(self):
self.repub = re.compile(u'\s*.diteur\s*', re.I)
self.reauteur = re.compile(u'\s*auteur.*', re.I)
self.reautclean = re.compile(u'\s*\(.*\)\s*')
def get_title(self, entry):
title = deepcopy(entry)
title.remove(title.find("dl[@title='Informations sur le livre']"))
title = ' '.join([i.text_content() for i in title.iterchildren()])
return unicode(title.replace('\n', ''))
def get_authors(self, entry):
author = entry.find("dl[@title='Informations sur le livre']")
authortext = []
for x in author.getiterator('dt'):
if self.reauteur.match(x.text):
elt = x.getnext()
while elt.tag == 'dd':
authortext.append(unicode(elt.text_content()))
elt = elt.getnext()
break
if len(authortext) == 1:
authortext = [self.reautclean.sub('', authortext[0])]
return authortext
def get_description(self, entry, verbose):
try:
return u'RESUME:\n' + unicode(entry.getparent().xpath("//p[@id='book-description']")[0].text)
except:
report(verbose)
return None
def get_book_info(self, entry, mi, verbose):
entry = entry.find("dl[@title='Informations sur le livre']")
for x in entry.getiterator('dt'):
if x.text == 'ISBN':
isbntext = x.getnext().text_content().replace('-', '')
if check_isbn(isbntext):
mi.isbn = unicode(isbntext)
elif self.repub.match(x.text):
mi.publisher = unicode(x.getnext().text_content())
elif x.text == 'Langue':
mi.language = unicode(x.getnext().text_content())
elif x.text == 'Date de parution':
d = x.getnext().text_content()
try:
default = utcnow().replace(day=15)
d = replace_months(d, 'fr')
d = parse_date(d, assume_utc=True, default=default)
mi.pubdate = d
except:
report(verbose)
return mi
def fill_MI(self, entry, title, authors, verbose):
mi = MetaInformation(title, authors)
mi.author_sort = authors_to_sort_string(authors)
mi.comments = self.get_description(entry, verbose)
return self.get_book_info(entry, mi, verbose)
def get_individual_metadata(self, browser, linkdata, verbose):
try:
raw = browser.open_novisit(self.BASE_URL + linkdata).read()
except Exception as e:
report(verbose)
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return
if isinstance(getattr(e, 'args', [None])[0], socket.timeout):
raise NiceBooksError(_('Nicebooks timed out. Try again later.'))
raise NiceBooksError(_('Nicebooks encountered an error.'))
if '<title>404 - ' in raw:
report(verbose)
return
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
try:
feed = soupparser.fromstring(raw)
except:
try:
#remove ASCII invalid chars
feed = soupparser.fromstring(clean_ascii_chars(raw))
except:
return None
# get results
return feed.xpath("//div[@id='container']")[0]
def populate(self, entries, browser, verbose=False):
#single entry
if len(entries) == 1 and not isinstance(entries[0], str):
try:
entry = entries[0].xpath("//div[@id='container']")[0]
entry = entry.find("div[@id='book-info']")
title = self.get_title(entry)
authors = self.get_authors(entry)
except Exception as e:
if verbose:
print 'Failed to get all details for an entry'
print e
return
self.append(self.fill_MI(entry, title, authors, verbose))
else:
#multiple entries
for x in entries:
try:
entry = self.get_individual_metadata(browser, x, verbose)
entry = entry.find("div[@id='book-info']")
title = self.get_title(entry)
authors = self.get_authors(entry)
except Exception as e:
if verbose:
print 'Failed to get all details for an entry'
print e
continue
self.append(self.fill_MI(entry, title, authors, verbose))
class Covers(object):
def __init__(self, isbn = None):
assert isbn is not None
self.urlimg = ''
self.isbn = isbn
self.isbnf = False
def __call__(self, entry = None):
try:
self.urlimg = entry.xpath("//div[@id='book-picture']/a")[0].get('href')
except:
return self
isbno = entry.get_element_by_id('book-info').find("dl[@title='Informations sur le livre']")
for x in isbno.getiterator('dt'):
if x.text == 'ISBN' and check_isbn(x.getnext().text_content()):
self.isbnf = True
break
return self
def check_cover(self):
return True if self.urlimg else False
def get_cover(self, browser, timeout = 5.):
try:
cover, ext = browser.open_novisit(self.urlimg, timeout=timeout).read(), \
self.urlimg.rpartition('.')[-1]
return cover, ext if ext else 'jpg'
except Exception as err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
raise NiceBooksError(_('Nicebooks timed out. Try again later.'))
if not len(self.urlimg):
if not self.isbnf:
raise ISBNNotFound(_('ISBN: %s not found.') % self.isbn)
raise NiceBooksError(_('An errror occured with Nicebooks cover fetcher'))
def search(title=None, author=None, publisher=None, isbn=None,
max_results=5, verbose=False, keywords=None):
br = browser()
entries = Query(title=title, author=author, isbn=isbn, publisher=publisher,
keywords=keywords, max_results=max_results)(br, verbose,timeout = 10.)
if entries is None or len(entries) == 0:
return None
#List of entry
ans = ResultList()
ans.populate(entries, br, verbose)
return ans
def check_for_cover(isbn):
br = browser()
entry = Query(isbn=isbn, max_results=1)(br, False)[0]
return Covers(isbn)(entry).check_cover()
def cover_from_isbn(isbn, timeout = 5.):
br = browser()
entry = Query(isbn=isbn, max_results=1)(br, False, timeout)[0]
return Covers(isbn)(entry).get_cover(br, timeout)
def option_parser():
parser = OptionParser(textwrap.dedent(\
_('''\
%prog [options]
Fetch book metadata from Nicebooks. You must specify one of title, author,
ISBN, publisher or keywords. Will fetch a maximum of 20 matches,
so you should make your query as specific as possible.
It can also get covers if the option is activated.
''')
))
parser.add_option('-t', '--title', help=_('Book title'))
parser.add_option('-a', '--author', help=_('Book author(s)'))
parser.add_option('-p', '--publisher', help=_('Book publisher'))
parser.add_option('-i', '--isbn', help=_('Book ISBN'))
parser.add_option('-k', '--keywords', help=_('Keywords'))
parser.add_option('-c', '--covers', default=0,
help=_('Covers: 1-Check/ 2-Download'))
parser.add_option('-p', '--coverspath', default='',
help=_('Covers files path'))
parser.add_option('-m', '--max-results', default=20,
help=_('Maximum number of results to fetch'))
parser.add_option('-v', '--verbose', default=0, action='count',
help=_('Be more verbose about errors'))
return parser
def main(args=sys.argv):
import os
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, isbn=opts.isbn, publisher=opts.publisher,
keywords=opts.keywords, verbose=opts.verbose, max_results=opts.max_results)
except AssertionError:
report(True)
parser.print_help()
return 1
if results is None or len(results) == 0:
print _('No result found for this search!')
return 0
for result in results:
print unicode(result).encode(preferred_encoding, 'replace')
covact = int(opts.covers)
if covact == 1:
textcover = _('No cover found!')
if check_for_cover(result.isbn):
textcover = _('A cover was found for this book')
print textcover
elif covact == 2:
cover_data, ext = cover_from_isbn(result.isbn)
cpath = result.isbn
if len(opts.coverspath):
cpath = os.path.normpath(opts.coverspath + '/' + result.isbn)
oname = os.path.abspath(cpath+'.'+ext)
open(oname, 'wb').write(cover_data)
print _('Cover saved to file '), oname
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -966,6 +966,8 @@ class OPF(object): # {{{
cover_id = covers[0].get('content')
for item in self.itermanifest():
if item.get('id', None) == cover_id:
mt = item.get('media-type', '')
if 'xml' not in mt:
return item.get('href', None)
@dynamic_property

View File

@ -307,7 +307,7 @@ class Source(Plugin):
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
[
# Remove things like: (2010) (Omnibus) etc.
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''),
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''),
# Remove any strings that contain the substring edition inside
# parentheses
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),

View File

@ -19,13 +19,8 @@ from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.sources.base import create_log
from calibre.ebooks.metadata.sources.identify import identify
from calibre.ebooks.metadata.sources.covers import download_cover
from calibre.utils.config import test_eight_code
def option_parser():
if not test_eight_code:
from calibre.ebooks.metadata.fetch import option_parser
return option_parser()
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
@ -48,9 +43,6 @@ def option_parser():
return parser
def main(args=sys.argv):
if not test_eight_code:
from calibre.ebooks.metadata.fetch import main
return main(args)
parser = option_parser()
opts, args = parser.parse_args(args)

View File

@ -13,6 +13,7 @@ from Queue import Queue, Empty
from threading import Thread
from io import BytesIO
from operator import attrgetter
from urlparse import urlparse
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
from calibre.ebooks.metadata.sources.base import create_log, msprefs
@ -400,6 +401,9 @@ def identify(log, abort, # {{{
and plugin.get_cached_cover_url(result.identifiers) is not
None)
result.identify_plugin = plugin
if msprefs['txt_comments']:
if plugin.has_html_comments and result.comments:
result.comments = html2text(r.comments)
log('The identify phase took %.2f seconds'%(time.time() - start_time))
log('The longest time (%f) was taken by:'%longest, lp)
@ -410,10 +414,6 @@ def identify(log, abort, # {{{
log('We have %d merged results, merging took: %.2f seconds' %
(len(results), time.time() - start_time))
if msprefs['txt_comments']:
for r in results:
if r.identify_plugin.has_html_comments and r.comments:
r.comments = html2text(r.comments)
max_tags = msprefs['max_tags']
for r in results:
@ -459,6 +459,14 @@ def urls_from_identifiers(identifiers): # {{{
if oclc:
ans.append(('OCLC', 'oclc', oclc,
'http://www.worldcat.org/oclc/'+oclc))
url = identifiers.get('uri', None)
if url is None:
url = identifiers.get('url', None)
if url and url.startswith('http'):
url = url[:8].replace('|', ':') + url[8:].replace('|', ',')
parts = urlparse(url)
name = parts.netloc
ans.append((name, 'url', url, url))
return ans
# }}}

View File

@ -41,7 +41,7 @@ class OverDrive(Source):
cached_cover_url_is_reliable = True
options = (
Option('get_full_metadata', 'bool', False,
Option('get_full_metadata', 'bool', True,
_('Download all metadata (slow)'),
_('Enable this option to gather all metadata available from Overdrive.')),
)

View File

@ -253,6 +253,8 @@ class MobiReader(object):
.italic { font-style: italic }
.underline { text-decoration: underline }
.mbp_pagebreak {
page-break-after: always; margin: 0; display: block
}
@ -601,6 +603,9 @@ class MobiReader(object):
elif tag.tag == 'i':
tag.tag = 'span'
tag.attrib['class'] = 'italic'
elif tag.tag == 'u':
tag.tag = 'span'
tag.attrib['class'] = 'underline'
elif tag.tag == 'b':
tag.tag = 'span'
tag.attrib['class'] = 'bold'

View File

@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en'
Convert an ODT file into a Open Ebook
'''
import os
from lxml import etree
from odf.odf2xhtml import ODF2XHTML
from calibre import CurrentDir, walk
@ -23,7 +25,51 @@ class Extract(ODF2XHTML):
with open(name, 'wb') as f:
f.write(data)
def __call__(self, stream, odir):
def filter_css(self, html, log):
root = etree.fromstring(html)
style = root.xpath('//*[local-name() = "style" and @type="text/css"]')
if style:
style = style[0]
css = style.text
if css:
style.text, sel_map = self.do_filter_css(css)
for x in root.xpath('//*[@class]'):
extra = []
orig = x.get('class')
for cls in orig.split():
extra.extend(sel_map.get(cls, []))
if extra:
x.set('class', orig + ' ' + ' '.join(extra))
html = etree.tostring(root, encoding='utf-8',
xml_declaration=True)
return html
def do_filter_css(self, css):
from cssutils import parseString
from cssutils.css import CSSRule
sheet = parseString(css)
rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
sel_map = {}
count = 0
for r in rules:
# Check if we have only class selectors for this rule
nc = [x for x in r.selectorList if not
x.selectorText.startswith('.')]
if len(r.selectorList) > 1 and not nc:
# Replace all the class selectors with a single class selector
# This will be added to the class attribute of all elements
# that have one of these selectors.
replace_name = 'c_odt%d'%count
count += 1
for sel in r.selectorList:
s = sel.selectorText[1:]
if s not in sel_map:
sel_map[s] = []
sel_map[s].append(replace_name)
r.selectorText = '.'+replace_name
return sheet.cssText, sel_map
def __call__(self, stream, odir, log):
from calibre.utils.zipfile import ZipFile
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator
@ -32,13 +78,17 @@ class Extract(ODF2XHTML):
if not os.path.exists(odir):
os.makedirs(odir)
with CurrentDir(odir):
print 'Extracting ODT file...'
log('Extracting ODT file...')
html = self.odf2xhtml(stream)
# A blanket img specification like this causes problems
# with EPUB output as the contaiing element often has
# with EPUB output as the containing element often has
# an absolute height and width set that is larger than
# the available screen real estate
html = html.replace('img { width: 100%; height: 100%; }', '')
try:
html = self.filter_css(html, log)
except:
log.exception('Failed to filter CSS, conversion may be slow')
with open('index.xhtml', 'wb') as f:
f.write(html.encode('utf-8'))
zf = ZipFile(stream, 'r')
@ -67,7 +117,7 @@ class ODTInput(InputFormatPlugin):
def convert(self, stream, options, file_ext, log,
accelerators):
return Extract()(stream, '.')
return Extract()(stream, '.', log)
def postprocess_book(self, oeb, opts, log):
# Fix <p><div> constructs as the asinine epubchecker complains

View File

@ -1049,8 +1049,8 @@ class Manifest(object):
# Remove hyperlinks with no content as they cause rendering
# artifacts in browser based renderers
# Also remove empty <b> and <i> tags
for a in xpath(data, '//h:a[@href]|//h:i|//h:b'):
# Also remove empty <b>, <u> and <i> tags
for a in xpath(data, '//h:a[@href]|//h:i|//h:b|//h:u'):
if a.get('id', None) is None and a.get('name', None) is None \
and len(a) == 0 and not a.text:
remove_elem(a)

View File

@ -124,15 +124,20 @@ class Stylizer(object):
def __init__(self, tree, path, oeb, opts, profile=None,
extra_css='', user_css=''):
from calibre.customize.ui import input_profiles
self.oeb, self.opts = oeb, opts
self.profile = None
for x in input_profiles():
if x.short_name == 'sony':
self.profile = profile
if self.profile is None:
# Use the default profile. This should really be using
# opts.output_profile, but I don't want to risk changing it, as
# doing so might well have hard to debug font size effects.
from calibre.customize.ui import output_profiles
for x in output_profiles():
if x.short_name == 'default':
self.profile = x
break
if self.profile is None:
self.profile = opts.input_profile
# Just in case the default profile is removed in the future :)
self.profile = opts.output_profile
self.logger = oeb.logger
item = oeb.manifest.hrefs[path]
basename = os.path.basename(path)

View File

@ -32,10 +32,11 @@ class PDFInput(InputFormatPlugin):
def convert_new(self, stream, accelerators):
from calibre.ebooks.pdf.reflow import PDFDocument
from calibre.utils.cleantext import clean_ascii_chars
if pdfreflow_err:
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
pdfreflow.reflow(stream.read(), 1, -1)
xml = open('index.xml', 'rb').read()
xml = clean_ascii_chars(open('index.xml', 'rb').read())
PDFDocument(xml, self.opts, self.log)
return os.path.join(os.getcwd(), 'metadata.opf')

View File

@ -15,7 +15,6 @@ import cStringIO
from lxml import etree
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.filenames import ascii_text
from calibre.utils.magick.draw import save_cover_data_to, identify_data
TAGS = {
@ -79,8 +78,7 @@ def txt2rtf(text):
elif val <= 127:
buf.write(x)
else:
repl = ascii_text(x)
c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl))
c = r'\u{0:d}?'.format(val)
buf.write(c)
return buf.getvalue()

View File

@ -34,7 +34,7 @@ if isosx:
)
gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books',
)
gprefs.defaults['action-layout-toolbar-device'] = (
@ -48,7 +48,7 @@ else:
gprefs.defaults['action-layout-menubar-device'] = ()
gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
)
gprefs.defaults['action-layout-toolbar-device'] = (

View File

@ -20,9 +20,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.filenames import ascii_filename
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import config, question_dialog
from calibre.gui2 import question_dialog
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import test_eight_code
from calibre.ebooks.metadata.sources.base import msprefs
def get_filters():
@ -180,7 +179,6 @@ class AddAction(InterfaceAction):
except IndexError:
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
self.isbn_add_dialog.accept()
if test_eight_code:
orig = msprefs['ignore_fields']
new = list(orig)
for x in ('title', 'authors'):
@ -192,14 +190,6 @@ class AddAction(InterfaceAction):
ids=self.add_by_isbn_ids)
finally:
msprefs['ignore_fields'] = orig
else:
orig = config['overwrite_author_title_metadata']
config['overwrite_author_title_metadata'] = True
try:
self.gui.iactions['Edit Metadata'].do_download_metadata(
self.add_by_isbn_ids)
finally:
config['overwrite_author_title_metadata'] = orig
return

View File

@ -246,7 +246,8 @@ class ChooseLibraryAction(InterfaceAction):
def delete_requested(self, name, location):
loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
_('All files from %s will be '
_('<b style="color: red">All files</b> (not just ebooks) '
'from <br><br><b>%s</b><br><br> will be '
'<b>permanently deleted</b>. Are you sure?') % loc,
show_copy_button=False):
return

View File

@ -10,15 +10,13 @@ from functools import partial
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
from calibre.gui2 import error_dialog, config, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2 import error_dialog, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.actions import InterfaceAction
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.icu import sort_key
from calibre.utils.config import test_eight_code
class EditMetadataAction(InterfaceAction):
@ -36,22 +34,8 @@ class EditMetadataAction(InterfaceAction):
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
if test_eight_code:
dall = self.download_metadata
else:
dall = partial(self.download_metadata_old, False, covers=True)
dident = partial(self.download_metadata_old, False, covers=False)
dcovers = partial(self.download_metadata_old, False, covers=True,
set_metadata=False, set_social_metadata=False)
md.addAction(_('Download metadata and covers'), dall,
md.addAction(_('Download metadata and covers'), self.download_metadata,
Qt.ControlModifier+Qt.Key_D)
if not test_eight_code:
md.addAction(_('Download only metadata'), dident)
md.addAction(_('Download only covers'), dcovers)
md.addAction(_('Download only social metadata'),
partial(self.download_metadata_old, False, covers=False,
set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
@ -88,7 +72,7 @@ class EditMetadataAction(InterfaceAction):
_('No books selected'), show=True)
db = self.gui.library_view.model().db
ids = [db.id(row.row()) for row in rows]
from calibre.gui2.metadata.bulk_download2 import start_download
from calibre.gui2.metadata.bulk_download import start_download
start_download(self.gui, ids,
Dispatcher(self.metadata_downloaded))
@ -96,7 +80,7 @@ class EditMetadataAction(InterfaceAction):
if job.failed:
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
return
from calibre.gui2.metadata.bulk_download2 import get_job_details
from calibre.gui2.metadata.bulk_download import get_job_details
id_map, failed_ids, failed_covers, all_failed, det_msg = \
get_job_details(job)
if all_failed:
@ -112,8 +96,9 @@ class EditMetadataAction(InterfaceAction):
show_copy_button = False
if failed_ids or failed_covers:
show_copy_button = True
num = len(failed_ids.union(failed_covers))
msg += '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%len(failed_ids)
' "Show details" to see which books.')%num
payload = (id_map, failed_ids, failed_covers)
from calibre.gui2.dialogs.message_box import ProceedNotification
@ -158,49 +143,6 @@ class EditMetadataAction(InterfaceAction):
self.apply_metadata_changes(id_map)
def download_metadata_old(self, checked, covers=True, set_metadata=True,
set_social_metadata=None):
rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
d = error_dialog(self.gui, _('Cannot download metadata'),
_('No books selected'))
d.exec_()
return
db = self.gui.library_view.model().db
ids = [db.id(row.row()) for row in rows]
self.do_download_metadata(ids, covers=covers,
set_metadata=set_metadata,
set_social_metadata=set_social_metadata)
def do_download_metadata(self, ids, covers=True, set_metadata=True,
set_social_metadata=None):
m = self.gui.library_view.model()
db = m.db
if set_social_metadata is None:
get_social_metadata = config['get_social_metadata']
else:
get_social_metadata = set_social_metadata
from calibre.gui2.metadata.bulk_download import DoDownload
if set_social_metadata is not None and set_social_metadata:
x = _('social metadata')
else:
x = _('covers') if covers and not set_metadata else _('metadata')
title = _('Downloading {0} for {1} book(s)').format(x, len(ids))
self._download_book_metadata = DoDownload(self.gui, title, db, ids,
get_covers=covers, set_metadata=set_metadata,
get_social_metadata=get_social_metadata)
m.stop_metadata_backup()
try:
self._download_book_metadata.exec_()
finally:
m.start_metadata_backup()
cr = self.gui.library_view.currentIndex().row()
x = self._download_book_metadata
if x.updated:
self.gui.library_view.model().refresh_ids(
x.updated, cr)
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
# }}}
def edit_metadata(self, checked, bulk=None):
@ -227,9 +169,7 @@ class EditMetadataAction(InterfaceAction):
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr)
func = (self.do_edit_metadata if test_eight_code else
self.do_edit_metadata_old)
changed, rows_to_refresh = func(row_list, current_row)
changed, rows_to_refresh = self.do_edit_metadata(row_list, current_row)
m = self.gui.library_view.model()
@ -244,36 +184,6 @@ class EditMetadataAction(InterfaceAction):
m.current_changed(current, previous)
self.gui.tags_view.recount()
def do_edit_metadata_old(self, row_list, current_row):
changed = set([])
db = self.gui.library_view.model().db
while True:
prev = next_ = None
if current_row > 0:
prev = db.title(row_list[current_row-1])
if current_row < len(row_list) - 1:
next_ = db.title(row_list[current_row+1])
d = MetadataSingleDialog(self.gui, row_list[current_row], db,
prev=prev, next_=next_)
d.view_format.connect(lambda
fmt:self.gui.iactions['View'].view_format(row_list[current_row],
fmt))
ret = d.exec_()
d.break_cycles()
if ret != d.Accepted:
break
changed.add(d.id)
self.gui.library_view.model().refresh_ids(list(d.books_to_refresh))
if d.row_delta == 0:
break
current_row += d.row_delta
self.gui.library_view.set_current_row(current_row)
self.gui.library_view.scroll_to_row(current_row)
return changed, set()
def do_edit_metadata(self, row_list, current_row):
from calibre.gui2.metadata.single import edit_metadata
db = self.gui.library_view.model().db
@ -613,6 +523,7 @@ class EditMetadataAction(InterfaceAction):
self.applied_ids, cr)
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
self.gui.tags_view.recount()
self.apply_id_map = []
self.apply_pd = None

View File

@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences
from calibre.gui2 import error_dialog
from calibre.constants import DEBUG
from calibre.constants import DEBUG, isosx
class PreferencesAction(InterfaceAction):
@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction):
def genesis(self):
pm = QMenu()
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
acname = _('Change calibre behavior') if isosx else _('Preferences')
pm.addAction(QIcon(I('config.png')), acname, self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
self.gui.run_wizard)
if not DEBUG:

View File

@ -60,7 +60,7 @@ class ViewAction(InterfaceAction):
def build_menus(self, db):
self.view_menu.clear()
self.view_menu.addAction(self.qaction)
self.view_menu.addAction(self.view_action)
self.view_menu.addAction(self.view_specific_action)
self.view_menu.addSeparator()
self.view_menu.addAction(self.action_pick_random)

View File

@ -62,8 +62,18 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
if isinstance(extra_customization_message, list):
self.opt_extra_customization = []
if len(extra_customization_message) > 6:
row_func = lambda x, y: ((x/2) * 2) + y
col_func = lambda x: x%2
else:
row_func = lambda x, y: x*2 + y
col_func = lambda x: 0
for i, m in enumerate(extra_customization_message):
label_text, tt = parse_msg(m)
if not label_text:
self.opt_extra_customization.append(None)
continue
if isinstance(settings.extra_customization[i], bool):
self.opt_extra_customization.append(QCheckBox(label_text))
self.opt_extra_customization[-1].setToolTip(tt)
@ -75,8 +85,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
l.setBuddy(self.opt_extra_customization[i])
l.setWordWrap(True)
self.opt_extra_customization[i].setText(settings.extra_customization[i])
self.extra_layout.addWidget(l)
self.extra_layout.addWidget(self.opt_extra_customization[i])
self.extra_layout.addWidget(l, row_func(i, 0), col_func(i))
self.extra_layout.addWidget(self.opt_extra_customization[i],
row_func(i, 1), col_func(i))
else:
self.opt_extra_customization = QLineEdit()
label_text, tt = parse_msg(extra_customization_message)
@ -86,8 +97,8 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
l.setWordWrap(True)
if settings.extra_customization:
self.opt_extra_customization.setText(settings.extra_customization)
self.extra_layout.addWidget(l)
self.extra_layout.addWidget(self.opt_extra_customization)
self.extra_layout.addWidget(l, 0, 0)
self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
self.opt_save_template.setText(settings.save_template)

View File

@ -101,7 +101,7 @@
</widget>
</item>
<item row="6" column="0">
<layout class="QVBoxLayout" name="extra_layout"/>
<layout class="QGridLayout" name="extra_layout"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">

View File

@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QAbstractItemView, QIcon,
QDialogButtonBox, QFrame, QLabel, QTimer, QMenu, QApplication)
from calibre.ebooks.metadata import author_to_author_sort
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
from calibre.utils.icu import sort_key, strcmp
from calibre.utils.icu import sort_key
class tableItem(QTableWidgetItem):
def __ge__(self, other):
@ -19,7 +20,7 @@ class tableItem(QTableWidgetItem):
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def __init__(self, parent, db, id_to_select):
def __init__(self, parent, db, id_to_select, select_sort):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
@ -30,14 +31,23 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.buttonBox.accepted.connect(self.accepted)
# Set up the column headings
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
self.down_arrow_icon = QIcon(I('arrow-down.png'))
self.up_arrow_icon = QIcon(I('arrow-up.png'))
self.blank_icon = QIcon(I('blank.png'))
self.auth_col = QTableWidgetItem(_('Author'))
self.table.setHorizontalHeaderItem(0, self.auth_col)
self.auth_col.setIcon(self.blank_icon)
self.aus_col = QTableWidgetItem(_('Author sort'))
self.table.setHorizontalHeaderItem(1, self.aus_col)
self.aus_col.setIcon(self.up_arrow_icon)
# Add the data
self.authors = {}
auts = db.get_authors_with_ids()
self.table.setRowCount(len(auts))
setattr(self.table, '__lt__', lambda x, y: True if strcmp(x, y) < 0 else False)
select_item = None
for row, (id, author, sort) in enumerate(auts):
author = author.replace('|', ',')
@ -48,7 +58,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.table.setItem(row, 0, aut)
self.table.setItem(row, 1, sort)
if id == id_to_select:
if select_sort:
select_item = sort
else:
select_item = aut
self.table.resizeColumnsToContents()
# set up the cellChanged signal only after the table is filled
@ -69,23 +82,153 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort)
self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author)
# Position on the desired item
if select_item is not None:
self.table.setCurrentItem(select_item)
self.table.editItem(select_item)
self.start_find_pos = select_item.row() * 2 + select_item.column()
else:
self.table.setCurrentCell(0, 0)
self.start_find_pos = -1
# set up the search box
self.find_box.initialize('manage_authors_search')
self.find_box.lineEdit().returnPressed.connect(self.do_find)
self.find_box.editTextChanged.connect(self.find_text_changed)
self.find_button.clicked.connect(self.do_find)
l = QLabel(self.table)
self.not_found_label = l
l.setFrameStyle(QFrame.StyledPanel)
l.setAutoFillBackground(True)
l.setText(_('No matches found'))
l.setAlignment(Qt.AlignVCenter)
l.resize(l.sizeHint())
l.move(10,20)
l.setVisible(False)
self.not_found_label.move(40, 40)
self.not_found_label_timer = QTimer()
self.not_found_label_timer.setSingleShot(True)
self.not_found_label_timer.timeout.connect(
self.not_found_label_timer_event, type=Qt.QueuedConnection)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested .connect(self.show_context_menu)
def show_context_menu(self, point):
self.context_item = self.table.itemAt(point)
case_menu = QMenu(_('Change Case'))
action_upper_case = case_menu.addAction(_('Upper Case'))
action_lower_case = case_menu.addAction(_('Lower Case'))
action_swap_case = case_menu.addAction(_('Swap Case'))
action_title_case = case_menu.addAction(_('Title Case'))
action_capitalize = case_menu.addAction(_('Capitalize'))
action_upper_case.triggered.connect(self.upper_case)
action_lower_case.triggered.connect(self.lower_case)
action_swap_case.triggered.connect(self.swap_case)
action_title_case.triggered.connect(self.title_case)
action_capitalize.triggered.connect(self.capitalize)
m = self.au_context_menu = QMenu()
ca = m.addAction(_('Copy'))
ca.triggered.connect(self.copy_to_clipboard)
ca = m.addAction(_('Paste'))
ca.triggered.connect(self.paste_from_clipboard)
m.addSeparator()
if self.context_item.column() == 0:
ca = m.addAction(_('Copy to author sort'))
ca.triggered.connect(self.copy_au_to_aus)
else:
ca = m.addAction(_('Copy to author'))
ca.triggered.connect(self.copy_aus_to_au)
m.addSeparator()
m.addMenu(case_menu)
m.exec_(self.table.mapToGlobal(point))
def copy_to_clipboard(self):
cb = QApplication.clipboard()
cb.setText(unicode(self.context_item.text()))
def paste_from_clipboard(self):
cb = QApplication.clipboard()
self.context_item.setText(cb.text())
def upper_case(self):
self.context_item.setText(icu_upper(unicode(self.context_item.text())))
def lower_case(self):
self.context_item.setText(icu_lower(unicode(self.context_item.text())))
def swap_case(self):
self.context_item.setText(unicode(self.context_item.text()).swapcase())
def title_case(self):
from calibre.utils.titlecase import titlecase
self.context_item.setText(titlecase(unicode(self.context_item.text())))
def capitalize(self):
from calibre.utils.icu import capitalize
self.context_item.setText(capitalize(unicode(self.context_item.text())))
def copy_aus_to_au(self):
row = self.context_item.row()
dest = self.table.item(row, 0)
dest.setText(self.context_item.text())
def copy_au_to_aus(self):
row = self.context_item.row()
dest = self.table.item(row, 1)
dest.setText(self.context_item.text())
def not_found_label_timer_event(self):
self.not_found_label.setVisible(False)
def find_text_changed(self):
self.start_find_pos = -1
def do_find(self):
self.not_found_label.setVisible(False)
# For some reason the button box keeps stealing the RETURN shortcut.
# Steal it back
self.buttonBox.button(QDialogButtonBox.Ok).setDefault(False)
self.buttonBox.button(QDialogButtonBox.Ok).setAutoDefault(False)
self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(False)
self.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
st = icu_lower(unicode(self.find_box.currentText()))
for i in range(0, self.table.rowCount()*2):
self.start_find_pos = (self.start_find_pos + 1) % (self.table.rowCount()*2)
r = (self.start_find_pos/2)%self.table.rowCount()
c = self.start_find_pos % 2
item = self.table.item(r, c)
text = icu_lower(unicode(item.text()))
if st in text:
self.table.setCurrentItem(item)
self.table.setFocus(True)
return
# Nothing found. Pop up the little dialog for 1.5 seconds
self.not_found_label.setVisible(True)
self.not_found_label_timer.start(1500)
def do_sort_by_author(self):
self.author_order = 1 if self.author_order == 0 else 0
self.table.sortByColumn(0, self.author_order)
self.sort_by_author.setChecked(True)
self.sort_by_author_sort.setChecked(False)
self.auth_col.setIcon(self.down_arrow_icon if self.author_order
else self.up_arrow_icon)
self.aus_col.setIcon(self.blank_icon)
def do_sort_by_author_sort(self):
self.author_sort_order = 1 if self.author_sort_order == 0 else 0
self.table.sortByColumn(1, self.author_sort_order)
self.sort_by_author.setChecked(False)
self.sort_by_author_sort.setChecked(True)
self.aus_col.setIcon(self.down_arrow_icon if self.author_sort_order
else self.up_arrow_icon)
self.auth_col.setIcon(self.blank_icon)
def accepted(self):
self.result = []

View File

@ -20,6 +20,50 @@
<string>Manage authors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="">
<item>
<widget class="QLabel">
<property name="text">
<string>&amp;Search for:</string>
</property>
<property name="buddy">
<cstring>find_box</cstring>
</property>
</widget>
</item>
<item>
<widget class="HistoryLineEdit" name="find_box">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="find_button">
<property name="text">
<string>F&amp;ind</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="table">
<property name="sizePolicy">
@ -143,4 +187,11 @@ after changing Preferences-&gt;Advanced-&gt;Tweaks-&gt;Author sort name algorith
</hints>
</connection>
</connections>
<customwidgets>
<customwidget>
<class>HistoryLineEdit</class>
<extends>QComboBox</extends>
<header>calibre/gui2/widgets.h</header>
</customwidget>
</customwidgets>
</ui>

View File

@ -1,271 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
GUI for fetching metadata from servers.
'''
import time
from threading import Thread
from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, pyqtSignal, \
QAbstractTableModel, QCoreApplication, QTimer
from PyQt4.QtGui import QDialog, QItemSelectionModel, QIcon
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog, config
from calibre.gui2.widgets import ProgressIndicator
from calibre import strftime, force_unicode
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
from calibre.utils.icu import sort_key
_hung_fetchers = set([])
class Fetcher(Thread):
def __init__(self, title, author, publisher, isbn, key):
Thread.__init__(self)
self.daemon = True
self.title = title
self.author = author
self.publisher = publisher
self.isbn = isbn
self.key = key
self.results, self.exceptions = [], []
def run(self):
from calibre.ebooks.metadata.fetch import search
self.results, self.exceptions = search(self.title, self.author,
self.publisher, self.isbn,
self.key if self.key else None)
class Matches(QAbstractTableModel):
def __init__(self, matches):
self.matches = matches
self.yes_icon = QVariant(QIcon(I('ok.png')))
QAbstractTableModel.__init__(self)
def rowCount(self, *args):
return len(self.matches)
def columnCount(self, *args):
return 8
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = _("Title")
elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Author Sort")
elif section == 3: text = _("Publisher")
elif section == 4: text = _("ISBN")
elif section == 5: text = _("Published")
elif section == 6: text = _("Has Cover")
elif section == 7: text = _("Has Summary")
return QVariant(text)
else:
return QVariant(section+1)
def summary(self, row):
return self.matches[row].comments
def data_as_text(self, book, col):
if col == 0 and book.title is not None:
return book.title
elif col == 1:
return ', '.join(book.authors)
elif col == 2 and book.author_sort is not None:
return book.author_sort
elif col == 3 and book.publisher is not None:
return book.publisher
elif col == 4 and book.isbn is not None:
return book.isbn
elif col == 5 and hasattr(book.pubdate, 'timetuple'):
return strftime('%b %Y', book.pubdate.timetuple())
elif col == 6 and book.has_cover:
return 'y'
elif col == 7 and book.comments:
return 'y'
return ''
def data(self, index, role):
row, col = index.row(), index.column()
book = self.matches[row]
if role == Qt.DisplayRole:
res = self.data_as_text(book, col)
if col <= 5 and res:
return QVariant(res)
return NONE
elif role == Qt.DecorationRole:
if col == 6 and book.has_cover:
return self.yes_icon
if col == 7 and book.comments:
return self.yes_icon
return NONE
def sort(self, col, order, reset=True):
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.matches.sort(None,
lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))),
descending)
if reset:
self.reset()
class FetchMetadata(QDialog, Ui_FetchMetadata):
HANG_TIME = 75 #seconds
queue_reject = pyqtSignal()
def __init__(self, parent, isbn, title, author, publisher, timeout):
QDialog.__init__(self, parent)
Ui_FetchMetadata.__init__(self)
self.setupUi(self)
for fetcher in list(_hung_fetchers):
if not fetcher.is_alive():
_hung_fetchers.remove(fetcher)
self.pi = ProgressIndicator(self)
self.timeout = timeout
QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata)
self.queue_reject.connect(self.reject, Qt.QueuedConnection)
isbndb_key = get_isbndb_key()
if not isbndb_key:
isbndb_key = ''
self.key.setText(isbndb_key)
self.setWindowTitle(title if title else _('Unknown'))
self.isbn = isbn
self.title = title
self.author = author.strip()
self.publisher = publisher
self.previous_row = None
self.warning.setVisible(False)
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
self.show_summary)
self.matches.setMouseTracking(True)
# Enabling sorting and setting a sort column will not change the initial
# order of the results, as they are filled in later
self.matches.setSortingEnabled(True)
self.matches.horizontalHeader().sectionClicked.connect(self.show_sort_indicator)
self.matches.horizontalHeader().setSortIndicatorShown(False)
self.fetch_metadata()
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
self.opt_auto_download_cover.setChecked(config['auto_download_cover'])
def show_summary(self, current, *args):
row = current.row()
if row != self.previous_row:
summ = self.model.summary(row)
self.summary.setText(summ if summ else '')
self.previous_row = row
def fetch_metadata(self):
self.warning.setVisible(False)
key = str(self.key.text())
if key:
set_isbndb_key(key)
else:
key = None
title = author = publisher = isbn = None
if self.isbn:
isbn = self.isbn
if self.title:
title = self.title
if self.author and not self.author == _('Unknown'):
author = self.author
self.fetch.setEnabled(False)
self.setCursor(Qt.WaitCursor)
QCoreApplication.instance().processEvents()
self.fetcher = Fetcher(title, author, publisher, isbn, key)
self.fetcher.start()
self.pi.start(_('Finding metadata...'))
self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck,
Qt.QueuedConnection)
self.start_time = time.time()
self._hangcheck.start(100)
def hangcheck(self):
if self.fetcher.is_alive() and \
time.time() - self.start_time < self.HANG_TIME:
return
self._hangcheck.stop()
try:
if self.fetcher.is_alive():
error_dialog(self, _('Could not find metadata'),
_('The metadata download seems to have stalled. '
'Try again later.')).exec_()
self.terminate()
return self.queue_reject.emit()
self.model = Matches(self.fetcher.results)
warnings = [(x[0], force_unicode(x[1])) for x in \
self.fetcher.exceptions if x[1] is not None]
if warnings:
warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings])
self.warning.setText('<p><b>'+ _('Warning')+':</b>'+\
_('Could not fetch metadata from:')+\
'<br>'+warnings+'</p>')
self.warning.setVisible(True)
if self.model.rowCount() < 1:
info_dialog(self, _('No metadata found'),
_('No metadata found, try adjusting the title and author '
'and/or removing the ISBN.')).exec_()
self.reject()
return
self.matches.setModel(self.model)
QObject.connect(self.matches.selectionModel(),
SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self.show_summary)
self.model.reset()
self.matches.selectionModel().select(self.model.index(0, 0),
QItemSelectionModel.Select | QItemSelectionModel.Rows)
self.matches.setCurrentIndex(self.model.index(0, 0))
finally:
self.fetch.setEnabled(True)
self.unsetCursor()
self.matches.resizeColumnsToContents()
self.pi.stop()
def terminate(self):
if hasattr(self, 'fetcher') and self.fetcher.is_alive():
_hung_fetchers.add(self.fetcher)
if hasattr(self, '_hangcheck') and self._hangcheck.isActive():
self._hangcheck.stop()
# Save value of auto_download_cover, since this is the only place it can
# be set. The values of the other options can be set in
# Preferences->Behavior and should not be set here as they affect bulk
# downloading as well.
if self.opt_auto_download_cover.isChecked() != config['auto_download_cover']:
config.set('auto_download_cover', self.opt_auto_download_cover.isChecked())
def __enter__(self, *args):
return self
def __exit__(self, *args):
self.terminate()
def selected_book(self):
try:
return self.matches.model().matches[self.matches.currentIndex().row()]
except:
return None
def chosen(self, index):
self.matches.setCurrentIndex(index)
self.accept()
def show_sort_indicator(self, *args):
self.matches.horizontalHeader().setSortIndicatorShown(True)

View File

@ -1,179 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FetchMetadata</class>
<widget class="QDialog" name="FetchMetadata">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>890</width>
<height>642</height>
</rect>
</property>
<property name="windowTitle">
<string>Fetch metadata</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/metadata.png</normaloff>:/images/metadata.png</iconset>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="tlabel">
<property name="text">
<string>&lt;p&gt;calibre can find metadata for your books from two locations: &lt;b&gt;Google Books&lt;/b&gt; and &lt;b&gt;isbndb.com&lt;/b&gt;. &lt;p&gt;To use isbndb.com you must sign up for a &lt;a href=&quot;http://www.isbndb.com&quot;&gt;free account&lt;/a&gt; and enter your access key below.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Access Key:</string>
</property>
<property name="buddy">
<cstring>key</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="key"/>
</item>
<item>
<widget class="QPushButton" name="fetch">
<property name="text">
<string>Fetch</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="warning">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Matches</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Select the book that most closely matches your copy from the list below</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="matches">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="summary"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_overwrite_author_title_metadata">
<property name="text">
<string>Overwrite author and title with author and title of selected book</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_get_social_metadata">
<property name="text">
<string>Download &amp;social metadata (tags/rating/etc.) for the selected book</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_auto_download_cover">
<property name="text">
<string>Automatically download the cover, if available</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>FetchMetadata</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>460</x>
<y>599</y>
</hint>
<hint type="destinationlabel">
<x>657</x>
<y>530</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>FetchMetadata</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>417</x>
<y>599</y>
</hint>
<hint type="destinationlabel">
<x>0</x>
<y>491</y>
</hint>
</hints>
</connection>
</connections>
</ui>

File diff suppressed because it is too large Load Diff

View File

@ -1,937 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MetadataSingleDialog</class>
<widget class="QDialog" name="MetadataSingleDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>994</width>
<height>716</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Edit Meta Information</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit_input.png</normaloff>:/images/edit_input.png</iconset>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>986</width>
<height>677</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="central_widget">
<property name="minimumSize">
<size>
<width>800</width>
<height>665</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="central_tabWidgetPage1">
<attribute name="title">
<string>&amp;Basic metadata</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="meta_box">
<property name="title">
<string>Meta information</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Title: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="EnLineEdit" name="title">
<property name="toolTip">
<string>Change the title of this book</string>
</property>
</widget>
</item>
<item row="0" column="2" rowspan="4">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="auto_title_sort">
<property name="toolTip">
<string>Automatically create the title sort entry based on the current title entry.
Using this button to create title sort will change title sort from red to green.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/auto_author_sort.png</normaloff>:/images/auto_author_sort.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="swap_button">
<property name="toolTip">
<string>Swap the author and title</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/swap.png</normaloff>:/images/swap.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="auto_author_sort">
<property name="toolTip">
<string>Automatically create the author sort entry based on the current author entry.
Using this button to create author sort will change author sort from red to green.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/auto_author_sort.png</normaloff>:/images/auto_author_sort.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title &amp;sort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>title_sort</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="title_sort">
<property name="toolTip">
<string>Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Author(s): </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="MultiCompleteComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Author S&amp;ort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>author_sort</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnLineEdit" name="author_sort">
<property name="toolTip">
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Rating:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QSpinBox" name="rating">
<property name="toolTip">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="whatsThis">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="suffix">
<string> stars</string>
</property>
<property name="maximum">
<number>5</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Publisher: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="MultiCompleteComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Ta&amp;gs: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="_2">
<item>
<widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="2">
<widget class="QToolButton" name="tag_editor_button">
<property name="toolTip">
<string>Open Tag Editor</string>
</property>
<property name="text">
<string>Open Tag Editor</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Series:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="_3">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="MultiCompleteComboBox" name="series">
<property name="toolTip">
<string>List of known series. You can add new series.</string>
</property>
<property name="whatsThis">
<string>List of known series. You can add new series.</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="2">
<widget class="QToolButton" name="remove_series_button">
<property name="toolTip">
<string>Remove unused series (Series that have no books)</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
<item row="8" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="series_index">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>Book </string>
</property>
<property name="maximum">
<double>99999999.989999994635582</double>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>IS&amp;BN:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>isbn</cstring>
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QLineEdit" name="isbn"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Date:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>date</cstring>
</property>
</widget>
</item>
<item row="10" column="1" colspan="2">
<widget class="QDateEdit" name="date">
<property name="displayFormat">
<string>dd MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Publishe&amp;d:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QDateEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="2">
<widget class="QToolButton" name="clear_pubdate_button">
<property name="toolTip">
<string>Clear published date</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="fetch_metadata_button">
<property name="text">
<string>&amp;Fetch metadata from server</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="layoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="bc_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Book Cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Change &amp;cover image:</string>
</property>
<property name="buddy">
<cstring>cover_button</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="cover_button">
<property name="text">
<string>&amp;Browse</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="trim_cover_button">
<property name="toolTip">
<string>Remove border (if any) from cover</string>
</property>
<property name="text">
<string>T&amp;rim</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reset_cover">
<property name="toolTip">
<string>Reset cover to default</string>
</property>
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QPushButton" name="fetch_cover_button">
<property name="text">
<string>Download co&amp;ver</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="af_group_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Available Formats</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1" rowspan="3">
<widget class="FormatList" name="formats">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>140</height>
</size>
</property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="iconSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="add_format_button">
<property name="toolTip">
<string>Add a new format for this book to the database</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/add_book.png</normaloff>:/images/add_book.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="remove_format_button">
<property name="toolTip">
<string>Remove the selected formats for this book from the database.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QToolButton" name="button_set_cover">
<property name="toolTip">
<string>Set the cover for the book from the selected format</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/book.png</normaloff>:/images/book.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QToolButton" name="button_set_metadata">
<property name="toolTip">
<string>Update metadata from the metadata in the selected format</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit_input.png</normaloff>:/images/edit_input.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<zorder></zorder>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>&amp;Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="Editor" name="comments" native="true"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Custom metadata</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2"/>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>EnLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>MultiCompleteComboBox</class>
<extends>QComboBox</extends>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>FormatList</class>
<extends>QListWidget</extends>
<header location="global">calibre/gui2/widgets.h</header>
</customwidget>
<customwidget>
<class>ImageView</class>
<extends>QWidget</extends>
<header>calibre/gui2/widgets.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>Editor</class>
<extends>QWidget</extends>
<header location="global">calibre/gui2/comments_editor.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>title</tabstop>
<tabstop>auto_title_sort</tabstop>
<tabstop>title_sort</tabstop>
<tabstop>swap_button</tabstop>
<tabstop>authors</tabstop>
<tabstop>auto_author_sort</tabstop>
<tabstop>author_sort</tabstop>
<tabstop>rating</tabstop>
<tabstop>publisher</tabstop>
<tabstop>tags</tabstop>
<tabstop>tag_editor_button</tabstop>
<tabstop>series</tabstop>
<tabstop>remove_series_button</tabstop>
<tabstop>series_index</tabstop>
<tabstop>isbn</tabstop>
<tabstop>date</tabstop>
<tabstop>pubdate</tabstop>
<tabstop>fetch_metadata_button</tabstop>
<tabstop>button_set_cover</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>formats</tabstop>
<tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop>
<tabstop>cover_button</tabstop>
<tabstop>trim_cover_button</tabstop>
<tabstop>reset_cover</tabstop>
<tabstop>fetch_cover_button</tabstop>
<tabstop>generate_cover_button</tabstop>
<tabstop>button_box</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop>
</tabstops>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>MetadataSingleDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>261</x>
<y>710</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>MetadataSingleDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>329</x>
<y>710</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -7,16 +7,16 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, shutil
from contextlib import closing
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog
from calibre.constants import isosx
from calibre.gui2 import open_local_file
from calibre.gui2 import open_local_file, error_dialog
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ptempfile import (PersistentTemporaryDirectory,
PersistentTemporaryFile)
class TweakEpub(QDialog, Ui_Dialog):
'''
@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog):
self.cancel_button.clicked.connect(self.reject)
self.explode_button.clicked.connect(self.explode)
self.rebuild_button.clicked.connect(self.rebuild)
self.preview_button.clicked.connect(self.preview)
# Position update dialog overlaying top left of app window
parent_loc = parent.pos()
self.move(parent_loc.x(),parent_loc.y())
self.gui = parent
self._preview_files = []
def cleanup(self):
if isosx:
try:
@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog):
# Delete directory containing exploded ePub
if self._exploded is not None:
shutil.rmtree(self._exploded, ignore_errors=True)
for x in self._preview_files:
try:
os.remove(x)
except:
pass
def display_exploded(self):
'''
@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog):
self.rebuild_button.setEnabled(True)
self.explode_button.setEnabled(False)
def rebuild(self, *args):
self._output = os.path.join(self._exploded, 'rebuilt.epub')
with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
def do_rebuild(self, src):
with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf:
# Write mimetype
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
# Write everything else
@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog):
zfn = os.path.relpath(absfn,
self._exploded).replace(os.sep, '/')
zf.write(absfn, zfn)
def preview(self):
if not self._exploded:
return error_dialog(self, _('Cannot preview'),
_('You must first explode the epub before previewing.'),
show=True)
tf = PersistentTemporaryFile('.epub')
tf.close()
self._preview_files.append(tf.name)
self.do_rebuild(tf.name)
self.gui.iactions['View']._view_file(tf.name)
def rebuild(self, *args):
self._output = os.path.join(self._exploded, 'rebuilt.epub')
self.do_rebuild(self._output)
return QDialog.accept(self)

View File

@ -23,6 +23,16 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;p&gt;Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window &lt;b&gt;and the editor windows you used to edit files in the epub&lt;/b&gt;.&lt;/p&gt;&lt;p&gt;Rebuild the ePub, updating your calibre library.&lt;/p&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="explode_button">
<property name="statusTip">
@ -37,23 +47,6 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="rebuild_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="statusTip">
<string>Rebuild ePub from exploded contents</string>
</property>
<property name="text">
<string>&amp;Rebuild ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="cancel_button">
<property name="statusTip">
@ -68,13 +61,31 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;p&gt;Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window &lt;b&gt;and the editor windows you used to edit files in the epub&lt;/b&gt;.&lt;/p&gt;&lt;p&gt;Rebuild the ePub, updating your calibre library.&lt;/p&gt;</string>
<item row="3" column="1">
<widget class="QPushButton" name="rebuild_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
<property name="statusTip">
<string>Rebuild ePub from exploded contents</string>
</property>
<property name="text">
<string>&amp;Rebuild ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="preview_button">
<property name="text">
<string>&amp;Preview ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/view.png</normaloff>:/images/view.png</iconset>
</property>
</widget>
</item>

View File

@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{
receiver = partial(self._location_selected, name)
ac.triggered.connect(receiver)
self.tooltips[name] = tooltip
if name != 'library':
m = QMenu(parent)
self._mem.append(m)
a = m.addAction(icon, tooltip)
a.triggered.connect(receiver)
if name != 'library':
self._mem.append(a)
a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
a.triggered.connect(self._eject_requested)
ac.setMenu(m)
self._mem.append(a)
else:
ac.setToolTip(tooltip)
ac.setMenu(m)
ac.calibre_name = name
return ac
@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{
def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
switch_actions, choose_action):
self.switch_menu = self.library_action.menu()
if self.switch_menu:
self.switch_menu.addSeparator()
else:
self.switch_menu = QMenu()
self.switch_menu.addAction(choose_action)
self.cs_menus = []
for t, acs in [(_('Quick switch'), quick_actions),
@ -85,6 +91,8 @@ class LocationManager(QObject): # {{{
self.switch_menu.addSeparator()
for ac in switch_actions:
self.switch_menu.addAction(ac)
if self.switch_menu != self.library_action.menu():
self.library_action.setMenu(self.switch_menu)
def _location_selected(self, location, *args):

View File

@ -439,10 +439,16 @@ class BooksView(QTableView): # {{{
if tweaks['sort_columns_at_startup'] is not None:
sh = []
try:
for c,d in tweaks['sort_columns_at_startup']:
if not isinstance(d, bool):
d = True if d == 0 else False
sh.append((c, d))
except:
# Ignore invalid tweak values as users seem to often get them
# wrong
import traceback
traceback.print_exc()
old_state['sort_history'] = sh
self.apply_state(old_state)

View File

@ -299,13 +299,13 @@ def run_gui(opts, args, actions, listener, app, gui_debug=None):
if getattr(runner.main, 'debug_on_restart', False):
run_in_debug_mode()
else:
import subprocess
print 'Restarting with:', e, sys.argv
if hasattr(sys, 'frameworks_dir'):
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
import subprocess
subprocess.Popen('sleep 3s; open '+app, shell=True)
else:
os.execvp(e, sys.argv)
subprocess.Popen([e] + sys.argv[1:])
else:
if iswindows:
try:

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import textwrap, re, os
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal,
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal, QMessageBox,
QIcon, QToolButton, QWidget, QLabel, QGridLayout,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
@ -19,10 +19,10 @@ from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import (title_sort, authors_to_string,
string_to_authors, check_isbn)
string_to_authors, check_isbn, authors_to_sort_string)
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE,
choose_files, error_dialog, choose_images, question_dialog)
choose_files, error_dialog, choose_images)
from calibre.utils.date import local_tz, qt_to_dt
from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS
@ -31,6 +31,16 @@ from calibre.utils.date import utcfromtimestamp
from calibre.gui2.comments_editor import Editor
from calibre.library.comments import comments_to_html
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.utils.icu import strcmp
def save_dialog(parent, title, msg, det_msg=''):
d = QMessageBox(parent)
d.setWindowTitle(title)
d.setText(msg)
d.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
return d.exec_()
'''
The interface common to all widgets used to set basic metadata
@ -156,7 +166,7 @@ class AuthorsEdit(MultiCompleteComboBox):
TOOLTIP = ''
LABEL = _('&Author(s):')
def __init__(self, parent):
def __init__(self, parent, manage_authors):
self.dialog = parent
self.books_to_refresh = set([])
MultiCompleteComboBox.__init__(self, parent)
@ -164,6 +174,28 @@ class AuthorsEdit(MultiCompleteComboBox):
self.setWhatsThis(self.TOOLTIP)
self.setEditable(True)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
manage_authors.triggered.connect(self.manage_authors)
def manage_authors(self):
if self.original_val != self.current_val:
d = save_dialog(self, _('Authors changed'),
_('You have changed the authors for this book. You must save '
'these changes before you can use Manage authors. Do you '
'want to save these changes?'))
if d == QMessageBox.Cancel:
return
if d == QMessageBox.Yes:
self.commit(self.db, self.id_)
self.db.commit()
self.original_val = self.current_val
else:
self.current_val = self.original_val
first_author = self.current_val[0] if len(self.current_val) else None
first_author_id = self.db.get_author_id(first_author) if first_author else None
self.dialog.parent().do_author_sort_edit(self, first_author_id,
select_sort=False)
self.initialize(self.db, self.id_)
self.dialog.author_sort.initialize(self.db, self.id_)
def get_default(self):
return _('Unknown')
@ -175,8 +207,8 @@ class AuthorsEdit(MultiCompleteComboBox):
self.clear()
for i in all_authors:
id, name = i
name = [name.strip().replace('|', ',') for n in name.split(',')]
self.addItem(authors_to_string(name))
name = name.strip().replace('|', ',')
self.addItem(name)
self.set_separator('&')
self.set_space_before_sep(True)
@ -188,6 +220,8 @@ class AuthorsEdit(MultiCompleteComboBox):
au = _('Unknown')
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
self.original_val = self.current_val
self.id_ = id_
self.db = db
def commit(self, db, id_):
authors = self.current_val
@ -238,7 +272,7 @@ class AuthorSortEdit(EnLineEdit):
'No action is required if this is what you want.'))
self.tooltips = (ok_tooltip, bad_tooltip)
self.authors_edit.editTextChanged.connect(self.update_state)
self.authors_edit.editTextChanged.connect(self.update_state_and_val)
self.textChanged.connect(self.update_state)
autogen_button.clicked.connect(self.auto_generate)
@ -260,12 +294,19 @@ class AuthorSortEdit(EnLineEdit):
return property(fget=fget, fset=fset)
def update_state_and_val(self):
# Handle case change if the authors box changed
aus = authors_to_sort_string(self.authors_edit.current_val)
if strcmp(aus, self.current_val) == 0:
self.current_val = aus
self.update_state()
def update_state(self, *args):
au = unicode(self.authors_edit.text())
au = re.sub(r'\s+et al\.$', '', au)
au = self.db.author_sort_from_authors(string_to_authors(au))
normal = au == self.current_val
normal = strcmp(au, self.current_val) == 0
if normal:
col = 'rgb(0, 255, 0, 20%)'
else:
@ -900,10 +941,13 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
def edit(self, db, id_):
if self.changed:
if question_dialog(self, _('Tags changed'),
d = save_dialog(self, _('Tags changed'),
_('You have changed the tags. In order to use the tags'
' editor, you must either discard or apply these '
'changes. Apply changes?'), show_copy_button=False):
'changes. Apply changes?'))
if d == QMessageBox.Cancel:
return
if d == QMessageBox.Yes:
self.commit(db, id_)
db.commit()
self.original_val = self.current_val

View File

@ -1,308 +1,195 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import traceback
from threading import Thread
from Queue import Queue, Empty
from functools import partial
from itertools import izip
from threading import Event
from PyQt4.Qt import QObject, QTimer, QDialog, \
QVBoxLayout, QTextBrowser, QLabel, QGroupBox, QDialogButtonBox
from PyQt4.Qt import (QIcon, QDialog,
QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
from calibre.ebooks.metadata.fetch import search, get_social_metadata
from calibre.gui2 import config, error_dialog
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.ebooks.metadata.covers import download_cover
from calibre.customize.ui import get_isbndb_key
from calibre.gui2.threaded_jobs import ThreadedJob
from calibre.ebooks.metadata.sources.identify import identify, msprefs
from calibre.ebooks.metadata.sources.covers import download_cover
from calibre.ebooks.metadata.book.base import Metadata
from calibre.customize.ui import metadata_plugins
from calibre.ptempfile import PersistentTemporaryFile
class Worker(Thread):
'Cover downloader'
# Start download {{{
def show_config(gui, parent):
from calibre.gui2.preferences import show_config_widget
show_config_widget('Sharing', 'Metadata download', parent=parent,
gui=gui, never_shutdown=True)
def __init__(self):
Thread.__init__(self)
self.daemon = True
self.jobs = Queue()
self.results = Queue()
class ConfirmDialog(QDialog):
def run(self):
while True:
id, mi = self.jobs.get()
if not getattr(mi, 'isbn', False):
break
try:
cdata, errors = download_cover(mi)
if cdata:
self.results.put((id, mi, True, cdata))
else:
msg = []
for e in errors:
if not e[0]:
msg.append(e[-1] + ' - ' + e[1])
self.results.put((id, mi, False, '\n'.join(msg)))
except:
self.results.put((id, mi, False, traceback.format_exc()))
def __init__(self, ids, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Schedule download?'))
self.setWindowIcon(QIcon(I('dialog_question.png')))
def __enter__(self):
self.start()
return self
l = self.l = QGridLayout()
self.setLayout(l)
def __exit__(self, *args):
self.jobs.put((False, False))
i = QLabel(self)
i.setPixmap(QPixmap(I('dialog_question.png')))
l.addWidget(i, 0, 0)
t = QLabel(
'<p>'+_('The download of metadata for the <b>%d selected book(s)</b> will'
' run in the background. Proceed?')%len(ids) +
'<p>'+_('You can monitor the progress of the download '
'by clicking the rotating spinner in the bottom right '
'corner.') +
'<p>'+_('When the download completes you will be asked for'
' confirmation before calibre applies the downloaded metadata.')
)
t.setWordWrap(True)
l.addWidget(t, 0, 1)
l.setColumnStretch(0, 1)
l.setColumnStretch(1, 100)
class DownloadMetadata(Thread):
'Metadata downloader'
self.identify = self.covers = True
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.bb.rejected.connect(self.reject)
b = self.bb.addButton(_('Download only &metadata'),
self.bb.AcceptRole)
b.clicked.connect(self.only_metadata)
b.setIcon(QIcon(I('edit_input.png')))
b = self.bb.addButton(_('Download only &covers'),
self.bb.AcceptRole)
b.clicked.connect(self.only_covers)
b.setIcon(QIcon(I('default_cover.png')))
b = self.b = self.bb.addButton(_('&Configure download'), self.bb.ActionRole)
b.setIcon(QIcon(I('config.png')))
b.clicked.connect(partial(show_config, parent, self))
l.addWidget(self.bb, 1, 0, 1, 2)
b = self.bb.addButton(_('Download &both'),
self.bb.AcceptRole)
b.clicked.connect(self.accept)
b.setDefault(True)
b.setAutoDefault(True)
b.setIcon(QIcon(I('ok.png')))
def __init__(self, db, ids, get_covers, set_metadata=True,
get_social_metadata=True):
Thread.__init__(self)
self.daemon = True
self.metadata = {}
self.covers = {}
self.set_metadata = set_metadata
self.get_social_metadata = get_social_metadata
self.social_metadata_exceptions = []
self.db = db
self.updated = set([])
self.get_covers = get_covers
self.worker = Worker()
self.results = Queue()
self.keep_going = True
for id in ids:
self.metadata[id] = db.get_metadata(id, index_is_id=True)
self.metadata[id].rating = None
self.total = len(ids)
if self.get_covers:
self.total += len(ids)
self.fetched_metadata = {}
self.fetched_covers = {}
self.failures = {}
self.cover_failures = {}
self.exception = self.tb = None
self.resize(self.sizeHint())
b.setFocus(Qt.OtherFocusReason)
def run(self):
try:
self._run()
except Exception as e:
self.exception = e
self.tb = traceback.format_exc()
def only_metadata(self):
self.covers = False
self.accept()
def _run(self):
self.key = get_isbndb_key()
if not self.key:
self.key = None
with self.worker:
for id, mi in self.metadata.items():
if not self.keep_going:
break
args = {}
if mi.isbn:
args['isbn'] = mi.isbn
else:
if mi.is_null('title'):
self.failures[id] = \
_('Book has neither title nor ISBN')
continue
args['title'] = mi.title
if mi.authors and mi.authors[0] != _('Unknown'):
args['author'] = mi.authors[0]
if self.key:
args['isbndb_key'] = self.key
results, exceptions = search(**args)
if results:
fmi = results[0]
self.fetched_metadata[id] = fmi
if self.get_covers:
if fmi.isbn:
self.worker.jobs.put((id, fmi))
else:
self.results.put((id, 'cover', False, mi.title))
if (not config['overwrite_author_title_metadata']):
fmi.authors = mi.authors
fmi.author_sort = mi.author_sort
fmi.title = mi.title
mi.smart_update(fmi)
if mi.isbn and self.get_social_metadata:
self.social_metadata_exceptions = get_social_metadata(mi)
if mi.rating:
mi.rating *= 2
if not self.get_social_metadata:
mi.tags = []
self.results.put((id, 'metadata', True, mi.title))
else:
self.failures[id] = _('No matches found for this book')
self.results.put((id, 'metadata', False, mi.title))
self.results.put((id, 'cover', False, mi.title))
self.commit_covers()
def only_covers(self):
self.identify = False
self.accept()
self.commit_covers(True)
def commit_covers(self, all=False):
if all:
self.worker.jobs.put((False, False))
while True:
try:
id, fmi, ok, cdata = self.worker.results.get_nowait()
if ok:
self.fetched_covers[id] = cdata
self.results.put((id, 'cover', ok, fmi.title))
else:
self.results.put((id, 'cover', ok, fmi.title))
try:
self.cover_failures[id] = unicode(cdata)
except:
self.cover_failures[id] = repr(cdata)
except Empty:
if not all or not self.worker.is_alive():
def start_download(gui, ids, callback):
d = ConfirmDialog(ids, gui)
ret = d.exec_()
d.b.clicked.disconnect()
if ret != d.Accepted:
return
class DoDownload(QObject):
job = ThreadedJob('metadata bulk download',
_('Download metadata for %d books')%len(ids),
download, (ids, gui.current_db, d.identify, d.covers), {}, callback)
gui.job_manager.run_threaded_job(job)
gui.status_bar.show_message(_('Metadata download started'), 3000)
# }}}
def __init__(self, parent, title, db, ids, get_covers, set_metadata=True,
get_social_metadata=True):
QObject.__init__(self, parent)
self.pd = ProgressDialog(title, min=0, max=0, parent=parent)
self.pd.canceled_signal.connect(self.cancel)
self.downloader = None
self.create = partial(DownloadMetadata, db, ids, get_covers,
set_metadata=set_metadata,
get_social_metadata=get_social_metadata)
self.get_covers = get_covers
self.db = db
self.updated = set([])
self.total = len(ids)
self.keep_going = True
def get_job_details(job):
id_map, failed_ids, failed_covers, title_map, all_failed = job.result
det_msg = []
for i in failed_ids | failed_covers:
title = title_map[i]
if i in failed_ids:
title += (' ' + _('(Failed metadata)'))
if i in failed_covers:
title += (' ' + _('(Failed cover)'))
det_msg.append(title)
det_msg = '\n'.join(det_msg)
return id_map, failed_ids, failed_covers, all_failed, det_msg
def exec_(self):
QTimer.singleShot(50, self.do_one)
ret = self.pd.exec_()
if getattr(self.downloader, 'exception', None) is not None and \
ret == self.pd.Accepted:
error_dialog(self.parent(), _('Failed'),
_('Failed to download metadata'), show=True)
else:
self.show_report()
return ret
def merge_result(oldmi, newmi):
dummy = Metadata(_('Unknown'))
for f in msprefs['ignore_fields']:
if ':' not in f:
setattr(newmi, f, getattr(dummy, f))
fields = set()
for plugin in metadata_plugins(['identify']):
fields |= plugin.touched_fields
def cancel(self, *args):
self.keep_going = False
self.downloader.keep_going = False
self.pd.reject()
for f in fields:
# Optimize so that set_metadata does not have to do extra work later
if not f.startswith('identifier:'):
if (not newmi.is_null(f) and getattr(newmi, f) == getattr(oldmi, f)):
setattr(newmi, f, getattr(dummy, f))
def do_one(self):
newmi.last_modified = oldmi.last_modified
return newmi
def download(ids, db, do_identify, covers,
log=None, abort=None, notifications=None):
ids = list(ids)
metadata = [db.get_metadata(i, index_is_id=True, get_user_categories=False)
for i in ids]
failed_ids = set()
failed_covers = set()
title_map = {}
ans = {}
count = 0
all_failed = True
'''
# Test apply dialog
all_failed = do_identify = covers = False
'''
for i, mi in izip(ids, metadata):
if abort.is_set():
log.error('Aborting...')
break
title, authors, identifiers = mi.title, mi.authors, mi.identifiers
title_map[i] = title
if do_identify:
results = []
try:
if not self.keep_going:
return
if self.downloader is None:
self.downloader = self.create()
self.downloader.start()
self.pd.set_min(0)
self.pd.set_max(self.downloader.total)
try:
r = self.downloader.results.get_nowait()
self.handle_result(r)
except Empty:
results = identify(log, Event(), title=title, authors=authors,
identifiers=identifiers)
except:
pass
if not self.downloader.is_alive():
while True:
try:
r = self.downloader.results.get_nowait()
self.handle_result(r)
except Empty:
break
self.pd.accept()
return
except:
self.cancel()
raise
QTimer.singleShot(50, self.do_one)
def handle_result(self, r):
id_, typ, ok, title = r
what = _('cover') if typ == 'cover' else _('metadata')
which = _('Downloaded') if ok else _('Failed to get')
if self.get_covers or typ != 'cover' or ok:
# Do not show message when cover fetch fails if user didn't ask to
# download covers
self.pd.set_msg(_('%s %s for: %s') % (which, what, title))
self.pd.value += 1
if ok:
self.updated.add(id_)
if typ == 'cover':
try:
self.db.set_cover(id_,
self.downloader.fetched_covers.pop(id_))
except:
self.downloader.cover_failures[id_] = \
traceback.format_exc()
if results:
all_failed = False
mi = merge_result(mi, results[0])
identifiers = mi.identifiers
if not mi.is_null('rating'):
# set_metadata expects a rating out of 10
mi.rating *= 2
else:
try:
self.set_metadata(id_)
except:
self.downloader.failures[id_] = \
traceback.format_exc()
def set_metadata(self, id_):
mi = self.downloader.metadata[id_]
if self.downloader.set_metadata:
self.db.set_metadata(id_, mi)
if not self.downloader.set_metadata and self.downloader.get_social_metadata:
if mi.rating:
self.db.set_rating(id_, mi.rating)
if mi.tags:
self.db.set_tags(id_, mi.tags)
if mi.comments:
self.db.set_comment(id_, mi.comments)
if mi.series:
self.db.set_series(id_, mi.series)
if mi.series_index is not None:
self.db.set_series_index(id_, mi.series_index)
def show_report(self):
f, cf = self.downloader.failures, self.downloader.cover_failures
report = []
if f:
report.append(
'<h3>Failed to download metadata for the following:</h3><ol>')
for id_, err in f.items():
mi = self.downloader.metadata[id_]
report.append('<li><b>%s</b><pre>%s</pre></li>' % (mi.title,
unicode(err)))
report.append('</ol>')
if cf:
report.append(
'<h3>Failed to download cover for the following:</h3><ol>')
for id_, err in cf.items():
mi = self.downloader.metadata[id_]
report.append('<li><b>%s</b><pre>%s</pre></li>' % (mi.title,
unicode(err)))
report.append('</ol>')
if len(self.updated) != self.total or report:
d = QDialog(self.parent())
bb = QDialogButtonBox(QDialogButtonBox.Ok, parent=d)
v1 = QVBoxLayout()
d.setLayout(v1)
d.setWindowTitle(_('Done'))
v1.addWidget(QLabel(_('Successfully downloaded metadata for %d out of %d books') %
(len(self.updated), self.total)))
gb = QGroupBox(_('Details'), self.parent())
v2 = QVBoxLayout()
gb.setLayout(v2)
b = QTextBrowser(self.parent())
v2.addWidget(b)
b.setHtml('\n'.join(report))
v1.addWidget(gb)
v1.addWidget(bb)
bb.accepted.connect(d.accept)
d.resize(800, 600)
d.exec_()
log.error('Failed to download metadata for', title)
failed_ids.add(i)
# We don't want set_metadata operating on anything but covers
mi = merge_result(mi, mi)
if covers:
cdata = download_cover(log, title=title, authors=authors,
identifiers=identifiers)
if cdata is not None:
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
f.write(cdata[-1])
mi.cover = f.name
all_failed = False
else:
failed_covers.add(i)
ans[i] = mi
count += 1
notifications.put((count/len(ids),
_('Downloaded %d of %d')%(count, len(ids))))
log('Download complete, with %d failures'%len(failed_ids))
return (ans, failed_ids, failed_covers, title_map, all_failed)

View File

@ -1,195 +0,0 @@
#!/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 functools import partial
from itertools import izip
from threading import Event
from PyQt4.Qt import (QIcon, QDialog,
QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
from calibre.gui2.threaded_jobs import ThreadedJob
from calibre.ebooks.metadata.sources.identify import identify, msprefs
from calibre.ebooks.metadata.sources.covers import download_cover
from calibre.ebooks.metadata.book.base import Metadata
from calibre.customize.ui import metadata_plugins
from calibre.ptempfile import PersistentTemporaryFile
# Start download {{{
def show_config(gui, parent):
from calibre.gui2.preferences import show_config_widget
show_config_widget('Sharing', 'Metadata download', parent=parent,
gui=gui, never_shutdown=True)
class ConfirmDialog(QDialog):
def __init__(self, ids, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Schedule download?'))
self.setWindowIcon(QIcon(I('dialog_question.png')))
l = self.l = QGridLayout()
self.setLayout(l)
i = QLabel(self)
i.setPixmap(QPixmap(I('dialog_question.png')))
l.addWidget(i, 0, 0)
t = QLabel(
'<p>'+_('The download of metadata for the <b>%d selected book(s)</b> will'
' run in the background. Proceed?')%len(ids) +
'<p>'+_('You can monitor the progress of the download '
'by clicking the rotating spinner in the bottom right '
'corner.') +
'<p>'+_('When the download completes you will be asked for'
' confirmation before calibre applies the downloaded metadata.')
)
t.setWordWrap(True)
l.addWidget(t, 0, 1)
l.setColumnStretch(0, 1)
l.setColumnStretch(1, 100)
self.identify = self.covers = True
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.bb.rejected.connect(self.reject)
b = self.bb.addButton(_('Download only &metadata'),
self.bb.AcceptRole)
b.clicked.connect(self.only_metadata)
b.setIcon(QIcon(I('edit_input.png')))
b = self.bb.addButton(_('Download only &covers'),
self.bb.AcceptRole)
b.clicked.connect(self.only_covers)
b.setIcon(QIcon(I('default_cover.png')))
b = self.b = self.bb.addButton(_('&Configure download'), self.bb.ActionRole)
b.setIcon(QIcon(I('config.png')))
b.clicked.connect(partial(show_config, parent, self))
l.addWidget(self.bb, 1, 0, 1, 2)
b = self.bb.addButton(_('Download &both'),
self.bb.AcceptRole)
b.clicked.connect(self.accept)
b.setDefault(True)
b.setAutoDefault(True)
b.setIcon(QIcon(I('ok.png')))
self.resize(self.sizeHint())
b.setFocus(Qt.OtherFocusReason)
def only_metadata(self):
self.covers = False
self.accept()
def only_covers(self):
self.identify = False
self.accept()
def start_download(gui, ids, callback):
d = ConfirmDialog(ids, gui)
ret = d.exec_()
d.b.clicked.disconnect()
if ret != d.Accepted:
return
job = ThreadedJob('metadata bulk download',
_('Download metadata for %d books')%len(ids),
download, (ids, gui.current_db, d.identify, d.covers), {}, callback)
gui.job_manager.run_threaded_job(job)
gui.status_bar.show_message(_('Metadata download started'), 3000)
# }}}
def get_job_details(job):
id_map, failed_ids, failed_covers, title_map, all_failed = job.result
det_msg = []
for i in failed_ids | failed_covers:
title = title_map[i]
if i in failed_ids:
title += (' ' + _('(Failed metadata)'))
if i in failed_covers:
title += (' ' + _('(Failed cover)'))
det_msg.append(title)
det_msg = '\n'.join(det_msg)
return id_map, failed_ids, failed_covers, all_failed, det_msg
def merge_result(oldmi, newmi):
dummy = Metadata(_('Unknown'))
for f in msprefs['ignore_fields']:
if ':' not in f:
setattr(newmi, f, getattr(dummy, f))
fields = set()
for plugin in metadata_plugins(['identify']):
fields |= plugin.touched_fields
for f in fields:
# Optimize so that set_metadata does not have to do extra work later
if not f.startswith('identifier:'):
if (not newmi.is_null(f) and getattr(newmi, f) == getattr(oldmi, f)):
setattr(newmi, f, getattr(dummy, f))
newmi.last_modified = oldmi.last_modified
return newmi
def download(ids, db, do_identify, covers,
log=None, abort=None, notifications=None):
ids = list(ids)
metadata = [db.get_metadata(i, index_is_id=True, get_user_categories=False)
for i in ids]
failed_ids = set()
failed_covers = set()
title_map = {}
ans = {}
count = 0
all_failed = True
'''
# Test apply dialog
all_failed = do_identify = covers = False
'''
for i, mi in izip(ids, metadata):
if abort.is_set():
log.error('Aborting...')
break
title, authors, identifiers = mi.title, mi.authors, mi.identifiers
title_map[i] = title
if do_identify:
results = []
try:
results = identify(log, Event(), title=title, authors=authors,
identifiers=identifiers)
except:
pass
if results:
all_failed = False
mi = merge_result(mi, results[0])
identifiers = mi.identifiers
if not mi.is_null('rating'):
# set_metadata expects a rating out of 10
mi.rating *= 2
else:
log.error('Failed to download metadata for', title)
failed_ids.add(i)
# We don't want set_metadata operating on anything but covers
mi = merge_result(mi, mi)
if covers:
cdata = download_cover(log, title=title, authors=authors,
identifiers=identifiers)
if cdata is not None:
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
f.write(cdata[-1])
mi.cover = f.name
all_failed = False
else:
failed_covers.add(i)
ans[i] = mi
count += 1
notifications.put((count/len(ids),
_('Downloaded %d of %d')%(count, len(ids))))
log('Download complete, with %d failures'%len(failed_ids))
return (ans, failed_ids, failed_covers, title_map, all_failed)

View File

@ -103,16 +103,18 @@ class MetadataSingleDialogBase(ResizableDialog):
self.basic_metadata_widgets.extend([self.title, self.title_sort])
self.deduce_author_sort_button = b = QToolButton(self)
b.setToolTip(_(
'Automatically create the author sort entry based on the current'
' author entry.\n'
'Using this button to create author sort will change author sort from'
' red to green.'))
b.setToolTip('<p>' +
_('Automatically create the author sort entry based on the current '
'author entry. Using this button to create author sort will '
'change author sort from red to green. There is a menu of '
'functions available under this button. Click and hold '
'on the button to see it.') + '</p>')
b.m = m = QMenu()
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
ac3 = m.addAction(QIcon(I('user_profile.png')), _('Manage authors'))
b.setMenu(m)
self.authors = AuthorsEdit(self)
self.authors = AuthorsEdit(self, ac3)
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
ac2)
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
@ -198,7 +200,7 @@ class MetadataSingleDialogBase(ResizableDialog):
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if before is not None and i == 0:
pass# Do something
pass
if len(ans[i+1].widgets) == 2:
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
@ -206,7 +208,7 @@ class MetadataSingleDialogBase(ResizableDialog):
for c in range(2, len(ans[i].widgets), 2):
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
if after is not None:
pass # Do something
pass
# }}}
def do_view_format(self, path, fmt):
@ -290,13 +292,17 @@ class MetadataSingleDialogBase(ResizableDialog):
show=True)
return
def update_from_mi(self, mi):
def update_from_mi(self, mi, update_sorts=True):
if not mi.is_null('title'):
self.title.current_val = mi.title
if update_sorts:
self.title_sort.auto_generate()
if not mi.is_null('authors'):
self.authors.current_val = mi.authors
if not mi.is_null('author_sort'):
self.author_sort.current_val = mi.author_sort
elif update_sorts:
self.author_sort.auto_generate()
if not mi.is_null('rating'):
try:
self.rating.current_val = mi.rating
@ -728,7 +734,135 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
# }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
def do_layout(self):
self.central_widget.clear()
self.labels = []
sto = QWidget.setTabOrder
self.central_widget.tabBar().setVisible(False)
tab0 = QWidget(self)
self.central_widget.addTab(tab0, _("&Metadata"))
l = QGridLayout()
tab0.setLayout(l)
# Basic metadata in col 0
tl = QGridLayout()
gb = QGroupBox(_('Basic metadata'), tab0)
l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
self.button_box.addButton(self.fetch_metadata_button,
QDialogButtonBox.ActionRole)
self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
self.config_metadata_button.setText(_('Configure metadata downloading'))
self.button_box.addButton(self.config_metadata_button,
QDialogButtonBox.ActionRole)
sto(self.button_box, self.title)
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
ql = BuddyLabel(widget)
tl.addWidget(ql, row, 1, 1, 1)
tl.addWidget(widget, row, 2, 1, 1)
if button is not None:
tl.addWidget(button, row, 3, span, 1)
if icon is not None:
button.setIcon(QIcon(I(icon)))
if tab_to is not None:
if button is not None:
sto(widget, button)
sto(button, tab_to)
else:
sto(widget, tab_to)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
create_row(0, self.title, self.title_sort,
button=self.deduce_title_sort_button, span=2,
icon='auto_author_sort.png')
create_row(1, self.title_sort, self.authors)
create_row(2, self.authors, self.author_sort,
button=self.deduce_author_sort_button,
span=2, icon='auto_author_sort.png')
create_row(3, self.author_sort, self.series)
create_row(4, self.series, self.series_index,
button=self.remove_unused_series_button, icon='trash.png')
create_row(5, self.series_index, self.tags)
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
create_row(7, self.rating, self.pubdate)
create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.timestamp)
create_row(10, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png')
create_row(11, self.identifiers, self.comments,
button=self.clear_identifiers_button, icon='trash.png')
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
12, 1, 1 ,1)
# Custom metadata in col 1
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
gb = QGroupBox(_('Custom metadata'), tab0)
gbl = QVBoxLayout()
gb.setLayout(gbl)
sr = QScrollArea(gb)
sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
sr.setWidgetResizable(True)
sr.setBackgroundRole(QPalette.Base)
sr.setFrameStyle(QFrame.NoFrame)
sr.setWidget(w)
gbl.addWidget(sr)
l.addWidget(gb, 0, 1, 1, 1)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Fixed)
sp.setVerticalPolicy(QSizePolicy.Expanding)
gb.setSizePolicy(sp)
self.set_custom_metadata_tab_order()
# comments span col 0 & 1
w = QGroupBox(_('Comments'), tab0)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Expanding)
sp.setVerticalPolicy(QSizePolicy.Expanding)
w.setSizePolicy(sp)
lb = QHBoxLayout()
w.setLayout(lb)
lb.addWidget(self.comments)
l.addWidget(w, 1, 0, 1, 2)
# Cover & formats in col 3
gb = QGroupBox(_('Cover'), tab0)
lb = QGridLayout()
gb.setLayout(lb)
lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter)
sto(self.clear_identifiers_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
lb.addWidget(b, 1, i, 1, 1)
sto(b, self.cover.buttons[i+1])
hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
lb.addLayout(hl, 2, 0, 1, 3)
l.addWidget(gb, 0, 2, 1, 1)
l.addWidget(self.formats_manager, 1, 2, 1, 1)
sto(self.cover.buttons[-1], self.formats_manager)
self.formats_manager.formats.setMaximumWidth(10000)
self.formats_manager.formats.setIconSize(QSize(32, 32))
# }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1,
'alt2': MetadataSingleDialogAlt2}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=None):

View File

@ -19,7 +19,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows
from calibre.utils.icu import sort_key
from calibre.utils.config import test_eight_code
class OutputFormatSetting(Setting):
@ -40,12 +39,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('network_timeout', prefs)
r('overwrite_author_title_metadata', config)
r('get_social_metadata', config)
if test_eight_code:
self.opt_overwrite_author_title_metadata.setVisible(False)
self.opt_get_social_metadata.setVisible(False)
r('new_version_notification', config)
r('upload_news_to_device', config)
r('delete_news_from_library_on_upload', config)
@ -67,13 +60,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal.connect(self.internally_viewed_formats_changed)
r('bools_are_tristate', db.prefs, restart_required=True)
if test_eight_code:
r = self.register
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')]
r('edit_metadata_single_layout', gprefs, choices=choices)
else:
self.opt_edit_metadata_single_layout.setVisible(False)
self.edit_metadata_single_label.setVisible(False)
def initialize(self):
ConfigWidgetBase.initialize(self)

View File

@ -14,41 +14,14 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="opt_overwrite_author_title_metadata">
<property name="text">
<string>&amp;Overwrite author and title by default when fetching metadata</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="opt_get_social_metadata">
<property name="text">
<string>Download &amp;social metadata (tags/ratings/etc.) by default</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="0">
<widget class="QCheckBox" name="opt_new_version_notification">
<property name="text">
<string>Show notification when &amp;new version is available</string>
</property>
</widget>
</item>
<item row="2" column="2">
<item row="1" column="1">
<widget class="QCheckBox" name="opt_bools_are_tristate">
<property name="toolTip">
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
@ -59,21 +32,21 @@ If not checked, the values can be Yes or No.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QCheckBox" name="opt_upload_news_to_device">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="4" column="2">
<item row="3" column="1">
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item row="6" column="0">
<item row="5" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_23">
@ -97,7 +70,7 @@ If not checked, the values can be Yes or No.</string>
</item>
</layout>
</item>
<item row="6" column="2">
<item row="5" column="1">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_2">
@ -130,7 +103,7 @@ If not checked, the values can be Yes or No.</string>
</item>
</layout>
</item>
<item row="8" column="0">
<item row="7" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="priority_label">
@ -169,7 +142,7 @@ If not checked, the values can be Yes or No.</string>
</item>
</layout>
</item>
<item row="8" column="2">
<item row="7" column="1">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_170">
@ -202,7 +175,7 @@ If not checked, the values can be Yes or No.</string>
</item>
</layout>
</item>
<item row="9" column="0">
<item row="8" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="edit_metadata_single_label">
@ -223,7 +196,7 @@ If not checked, the values can be Yes or No.</string>
</item>
</layout>
</item>
<item row="20" column="0">
<item row="19" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Preferred &amp;input format order:</string>
@ -285,7 +258,7 @@ If not checked, the values can be Yes or No.</string>
</layout>
</widget>
</item>
<item row="20" column="2">
<item row="19" column="1">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Use internal &amp;viewer for:</string>
@ -304,7 +277,7 @@ If not checked, the values can be Yes or No.</string>
</layout>
</widget>
</item>
<item row="9" column="2">
<item row="8" column="1">
<widget class="QPushButton" name="reset_confirmation_button">
<property name="text">
<string>Reset all disabled &amp;confirmation dialogs</string>

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QDialog, QVBoxLayout, QPlainTextEdit, QTimer, \
QDialogButtonBox, QPushButton, QApplication, QIcon, QMessageBox
from calibre.constants import iswindows
def step_dialog(parent, title, msg, det_msg=''):
d = QMessageBox(parent)
d.setWindowTitle(title)
d.setText(msg)
d.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
return d.exec_() & QMessageBox.Cancel
class UserDefinedDevice(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self._layout = QVBoxLayout(self)
self.setLayout(self._layout)
self.log = QPlainTextEdit(self)
self._layout.addWidget(self.log)
self.log.setPlainText(_('Getting device information')+'...')
self.copy = QPushButton(_('Copy to &clipboard'))
self.copy.setDefault(True)
self.setWindowTitle(_('User-defined device information'))
self.setWindowIcon(QIcon(I('debug.png')))
self.copy.clicked.connect(self.copy_to_clipboard)
self.ok = QPushButton('&OK')
self.ok.setAutoDefault(False)
self.ok.clicked.connect(self.accept)
self.bbox = QDialogButtonBox(self)
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
self._layout.addWidget(self.bbox)
self.resize(750, 500)
self.bbox.setEnabled(False)
QTimer.singleShot(1000, self.device_info)
def device_info(self):
try:
from calibre.devices import device_info
r = step_dialog(self.parent(), _('Device Detection'),
_('Ensure your device is disconnected, then press OK'))
if r:
self.close()
return
before = device_info()
r = step_dialog(self.parent(), _('Device Detection'),
_('Ensure your device is connected, then press OK'))
if r:
self.close()
return
after = device_info()
new_drives = after['drive_set'] - before['drive_set']
new_devices = after['device_set'] - before['device_set']
res = ''
if (not iswindows or len(new_drives)) and len(new_devices) == 1:
for d in new_devices:
res = _('USB Vendor ID (in hex)') + ': 0x' + \
after['device_details'][d][0] + '\n'
res += _('USB Product ID (in hex)') + ': 0x' + \
after['device_details'][d][1] + '\n'
res += _('USB Revision ID (in hex)') + ': 0x' + \
after['device_details'][d][2] + '\n'
if iswindows:
# sort the drives by the order number
for i,d in enumerate(sorted(new_drives,
key=lambda x: after['drive_details'][x][0])):
if i == 0:
res += _('Windows main memory ID string') + ': ' + \
after['drive_details'][d][1] + '\n'
res += _('Windows main memory ID string') + ': ' + \
after['drive_details'][d][2] + '\n'
else:
res += _('Windows card A vendor string') + ': ' + \
after['drive_details'][d][1] + '\n'
res += _('Windows card A ID string') + ': ' + \
after['drive_details'][d][2] + '\n'
trailer = _(
'Copy these values to the clipboard, paste them into an '
'editor, then enter them into the USER_DEVICE by '
'customizing the device plugin in Preferences->Plugins. '
'Remember to also enter the folders where you want the books to '
'be put. You must restart calibre for your changes '
'to take effect.\n')
self.log.setPlainText(res + '\n\n' + trailer)
finally:
self.bbox.setEnabled(True)
def copy_to_clipboard(self):
QApplication.clipboard().setText(self.log.toPlainText())
if __name__ == '__main__':
app = QApplication([])
d = UserDefinedDevice()
d.exec_()

View File

@ -190,7 +190,15 @@ class FieldsModel(QAbstractListModel): # {{{
return ans | Qt.ItemIsUserCheckable
def restore_defaults(self):
self.overrides = dict([(f, self.state(f, True)) for f in self.fields])
self.overrides = dict([(f, self.state(f, Qt.Checked)) for f in self.fields])
self.reset()
def select_all(self):
self.overrides = dict([(f, Qt.Checked) for f in self.fields])
self.reset()
def clear_all(self):
self.overrides = dict([(f, Qt.Unchecked) for f in self.fields])
self.reset()
def setData(self, index, val, role):
@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.fields_view.setModel(self.fields_model)
self.fields_model.dataChanged.connect(self.changed_signal)
self.select_all_button.clicked.connect(self.fields_model.select_all)
self.clear_all_button.clicked.connect(self.fields_model.clear_all)
def configure_plugin(self):
for index in self.sources_view.selectionModel().selectedRows():
plugin = self.sources_model.data(index, Qt.UserRole)

View File

@ -77,8 +77,8 @@
<property name="title">
<string>Downloaded metadata fields</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QListView" name="fields_view">
<property name="toolTip">
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
@ -88,6 +88,20 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="select_all_button">
<property name="text">
<string>&amp;Select all</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="clear_all_button">
<property name="text">
<string>&amp;Clear all</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -30,6 +30,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('enforce_cpu_limit', config, restart_required=True)
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.button_open_config_dir.clicked.connect(self.open_config_dir)
self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx)
@ -38,6 +39,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
d = DebugDevice(self)
d.exec_()
def user_defined_device(self, *args):
from calibre.gui2.preferences.device_user_defined import UserDefinedDevice
d = UserDefinedDevice(self)
d.exec_()
def open_config_dir(self, *args):
from calibre.utils.config import config_dir
open_local_file(config_dir)

View File

@ -58,7 +58,14 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="4" column="0" colspan="2">
<widget class="QPushButton" name="user_defined_device_button">
<property name="text">
<string>Get information to setup the &amp;user defined device</string>
</property>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@ -1,79 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time
from threading import Thread
from PyQt4.Qt import QDialog, QDialogButtonBox, Qt, QLabel, QVBoxLayout, \
QTimer
from calibre.ebooks.metadata import MetaInformation
class Worker(Thread):
def __init__(self, mi):
Thread.__init__(self)
self.daemon = True
self.mi = MetaInformation(mi)
self.exceptions = []
def run(self):
from calibre.ebooks.metadata.fetch import get_social_metadata
self.exceptions = get_social_metadata(self.mi)
class SocialMetadata(QDialog):
TIMEOUT = 300 # seconds
def __init__(self, mi, parent):
QDialog.__init__(self, parent)
self.bbox = QDialogButtonBox(QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.mi = mi
self.layout = QVBoxLayout(self)
self.label = QLabel(_('Downloading social metadata, please wait...'), self)
self.label.setWordWrap(True)
self.layout.addWidget(self.label)
self.layout.addWidget(self.bbox)
self.worker = Worker(mi)
self.bbox.rejected.connect(self.reject)
self.worker.start()
self.start_time = time.time()
self.timed_out = False
self.rejected = False
QTimer.singleShot(50, self.update)
def reject(self):
self.rejected = True
QDialog.reject(self)
def update(self):
if self.rejected:
return
if time.time() - self.start_time > self.TIMEOUT:
self.timed_out = True
self.reject()
return
if not self.worker.is_alive():
self.accept()
return
QTimer.singleShot(50, self.update)
def accept(self):
self.mi.tags = self.worker.mi.tags
self.mi.rating = self.worker.mi.rating
self.mi.comments = self.worker.mi.comments
if self.worker.mi.series:
self.mi.series = self.worker.mi.series
self.mi.series_index = self.worker.mi.series_index
QDialog.accept(self)
@property
def exceptions(self):
return self.worker.exceptions

View File

@ -155,6 +155,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
self.config['sort_col'] = self.results_view.model().sort_col
self.config['sort_order'] = self.results_view.model().sort_order
self.config['open_external'] = self.open_external.isChecked()
store_check = {}
for n in self.store_plugins:
@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog):
else:
self.resize_columns()
self.open_external.setChecked(self.config.get('open_external', False))
store_check = self.config.get('store_checked', None)
if store_check:
for n in store_check:
@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item)
self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
def check_progress(self):
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():

View File

@ -70,7 +70,7 @@
<x>0</x>
<y>0</y>
<width>215</width>
<height>116</height>
<height>93</height>
</rect>
</property>
</widget>
@ -101,6 +101,16 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="open_external">
<property name="toolTip">
<string>Open a selected book in the system's web browser</string>
</property>
<property name="text">
<string>Open in &amp;external browser</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter_2">

View File

@ -2048,12 +2048,12 @@ class TagBrowserMixin(object): # {{{
self.library_view.select_rows(ids)
# refreshing the tags view happens at the emit()/call() site
def do_author_sort_edit(self, parent, id):
def do_author_sort_edit(self, parent, id, select_sort=True):
'''
Open the manage authors dialog
'''
db = self.library_view.model().db
editor = EditAuthorsDialog(parent, db, id)
editor = EditAuthorsDialog(parent, db, id, select_sort)
d = editor.exec_()
if d:
for (id, old_author, new_author, new_sort) in editor.result:

View File

@ -8,6 +8,7 @@ from collections import namedtuple
from copy import deepcopy
from xml.sax.saxutils import escape
from lxml import etree
from types import StringType, UnicodeType
from calibre import prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding, DEBUG
@ -15,13 +16,16 @@ from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
from calibre.ebooks.chardet import substitute_entites
from calibre.library.save_to_disk import preprocess_template
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.bibtex import BibTeX
from calibre.utils.config import config_dir
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf
from calibre.utils.html2text import html2text
from calibre.utils.icu import capitalize
from calibre.utils.logging import default_log as log
from calibre.utils.zipfile import ZipFile, ZipInfo
from calibre.utils.magick.draw import thumbnail
from calibre.utils.zipfile import ZipFile, ZipInfo
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
@ -303,12 +307,6 @@ class BIBTEX(CatalogPlugin): # {{{
def run(self, path_to_output, opts, db, notification=DummyReporter()):
from types import StringType, UnicodeType
from calibre.library.save_to_disk import preprocess_template
#Bibtex functions
from calibre.utils.bibtex import BibTeX
def create_bibtex_entry(entry, fields, mode, template_citation,
bibtexdict, citation_bibtex=True, calibre_files=True):
@ -365,6 +363,11 @@ class BIBTEX(CatalogPlugin): # {{{
#\n removal
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
#html to text
try:
item = html2text(item)
except:
log.warn("Failed to convert comments to text")
bibtex_entry.append(u'note = "%s"' % bibtexdict.utf8ToBibtex(item))
elif field == 'isbn' :
@ -941,6 +944,7 @@ class EPUB_MOBI(CatalogPlugin):
catalog.createDirectoryStructure()
catalog.copyResources()
catalog.buildSources()
Options managed in gui2.catalog.catalog_epub_mobi.py
'''
# A single number creates 'Last x days' only.

View File

@ -33,7 +33,7 @@ from calibre import isbytestring
from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks, from_json, to_json
from calibre.utils.icu import sort_key
from calibre.utils.icu import sort_key, strcmp
from calibre.utils.search_query_parser import saved_searches, set_saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick.draw import save_cover_data_to
@ -1920,6 +1920,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
result.append(r)
return ' & '.join(result).replace('|', ',')
def _update_author_in_cache(self, id_, ss, final_authors):
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id_))
self.data.set(id_, self.FIELD_MAP['authors'],
','.join([a.replace(',', '|') for a in final_authors]),
row_is_id=True)
self.data.set(id_, self.FIELD_MAP['author_sort'], ss, row_is_id=True)
aum = self.authors_with_sort_strings(id_, index_is_id=True)
self.data.set(id_, self.FIELD_MAP['au_map'],
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
row_is_id=True)
def _set_authors(self, id, authors, allow_case_change=False):
if not authors:
authors = [_('Unknown')]
@ -1933,14 +1945,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
a = a.strip().replace(',', '|')
if not isinstance(a, unicode):
a = a.decode(preferred_encoding, 'replace')
aus = self.conn.get('SELECT id, name FROM authors WHERE name=?', (a,))
aus = self.conn.get('SELECT id, name, sort FROM authors WHERE name=?', (a,))
if aus:
aid, name = aus[0]
aid, name, sort = aus[0]
# Handle change of case
if name != a:
if allow_case_change:
self.conn.execute('''UPDATE authors
SET name=? WHERE id=?''', (a, aid))
ns = author_to_author_sort(a.replace('|', ','))
if strcmp(sort, ns) == 0:
sort = ns
self.conn.execute('''UPDATE authors SET name=?, sort=?
WHERE id=?''', (a, sort, aid))
case_change = True
else:
a = name
@ -1957,17 +1972,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
bks = self.conn.get('''SELECT book FROM books_authors_link
WHERE author=?''', (aid,))
books_to_refresh |= set([bk[0] for bk in bks])
for bk in books_to_refresh:
ss = self.author_sort_from_book(id, index_is_id=True)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id))
self.data.set(id, self.FIELD_MAP['authors'],
','.join([a.replace(',', '|') for a in final_authors]),
row_is_id=True)
self.data.set(id, self.FIELD_MAP['author_sort'], ss, row_is_id=True)
aum = self.authors_with_sort_strings(id, index_is_id=True)
self.data.set(id, self.FIELD_MAP['au_map'],
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
row_is_id=True)
aus = self.author_sort(bk, index_is_id=True)
if strcmp(aus, ss) == 0:
self._update_author_in_cache(bk, ss, final_authors)
# This can repeat what was done above in rare cases. Let it.
ss = self.author_sort_from_book(id, index_is_id=True)
self._update_author_in_cache(id, ss, final_authors)
return books_to_refresh
def set_authors(self, id, authors, notify=True, commit=True,
@ -2273,6 +2285,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return []
return result
def get_author_id(self, author):
author = author.replace(',', '|')
result = self.conn.get('SELECT id FROM authors WHERE name=?',
(author,), all=False)
return result
def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
(new_sort.strip(), old_id))

View File

@ -102,6 +102,8 @@ What devices does |app| support?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
At the moment |app| has full support for the SONY PRS line, Barnes & Noble Nook line, Cybook Gen 3/Opus, Amazon Kindle line, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook line, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3 and clones, SpringDesign Alex, Kobo Reader, various Android phones and the iPhone/iPad. In addition, using the :guilabel:`Connect to folder` function you can use it with any ebook reader that exports itself as a USB disk.
There is also a special ``User Defined`` device plugin that can be used to connect to arbitrary devices that present their memory as disk drives. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscelleaneous -> Get information to setup the user defined device`` for more information.
How can I help get my device supported in |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -133,6 +135,11 @@ Follow these steps to find the problem:
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker <http://bugs.calibre-ebook.com>`_.
My device is non-standard or unusual. What can I do to connect to it?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -468,6 +475,18 @@ If it still wont launch, start a command prompt (press the windows key and R; th
Post any output you see in a help message on the `Forum <http://www.mobileread.com/forums/forumdisplay.php?f=166>`_.
|app| freezes when I click on anything?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are three possible things I know of, that can cause this:
* You recently connected an external monitor or TV to your computer. In this case, whenever |app| opens a new window like the edit metadata window or the conversion dialog, it appears on the second monitor where you dont notice it and so you think |app| has frozen. Disconnect your second monitor and restart calibre.
* You are using a Wacom branded mouse. There is an incompatibility between Wacom mice and the graphics toolkit |app| uses. Try using a non-Wacom mouse.
* You have invalid files in your fonts folder. If this is the case, start |app| in debug mode as desribed in the previous answer and you will get messages about invalid files in :file:`C:\\Windows\\fonts`. Delete these files and you will be fine.
|app| is not starting on OS X?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -545,7 +564,7 @@ You have two choices:
How is |app| licensed?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode <http://code.google.com/p/calibre-ebook/downloads/list>`_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without maing your software open source. For details, see `The GNU GPL v3 <http://www.gnu.org/licenses/gpl.html>`_.
|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode <http://code.google.com/p/calibre-ebook/downloads/list>`_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without making your software open source. For details, see `The GNU GPL v3 <http://www.gnu.org/licenses/gpl.html>`_.
How do I run calibre from my USB stick?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -19,7 +19,7 @@ Editing the metadata of one book at a time
Click the book you want to edit and then click the :guilabel:`Edit metadata` button or press the ``E`` key. A dialog opens that allows you to edit all aspects of the metadata. It has various features to make editing faster and more efficient. A list of the commonly used tips:
* You can click the button in between title and authors to swap them automatically.
* You can click the button next to author sort to automatically to have |app| automatically fill it from the author name.
* You can click the button next to author sort to have |app| automatically fill it in using the sort values stored with each author. Use the :guilabel:`Manage authors` dialog to see and change the authors' sort values. This dialog can be opened by clicking and holding the button next to author sort.
* You can click the button next to tags to use the Tag Editor to manage the tags associated with the book.
* The ISBN box will have a red background if you enter an invalid ISBN. It will be green for valid ISBNs
* The author sort box will be red if the author sort value differs from what |app| thinks it should be.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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