mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
3d519c3be1
@ -19,6 +19,34 @@
|
|||||||
# new recipes:
|
# new recipes:
|
||||||
# - title:
|
# - 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
|
- version: 0.7.58
|
||||||
date: 2011-04-29
|
date: 2011-04-29
|
||||||
|
|
||||||
|
16
recipes/auto_blog.recipe
Normal file
16
recipes/auto_blog.recipe
Normal 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/'
|
||||||
|
|
||||||
|
|
12
recipes/big_picture.recipe
Normal file
12
recipes/big_picture.recipe
Normal 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')]
|
@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>'
|
__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 '''
|
''' http://brandeins.de - Wirtschaftsmagazin '''
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
@ -13,8 +14,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
|||||||
class BrandEins(BasicNewsRecipe):
|
class BrandEins(BasicNewsRecipe):
|
||||||
|
|
||||||
title = u'brand eins'
|
title = u'brand eins'
|
||||||
__author__ = 'Constantin Hofstetter; Steffen Siebert'
|
__author__ = 'Constantin Hofstetter'
|
||||||
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.'
|
description = u'Wirtschaftsmagazin'
|
||||||
publisher ='brandeins.de'
|
publisher ='brandeins.de'
|
||||||
category = 'politics, business, wirtschaft, Germany'
|
category = 'politics, business, wirtschaft, Germany'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
@ -105,10 +106,11 @@ class BrandEins(BasicNewsRecipe):
|
|||||||
keys = issue_map.keys()
|
keys = issue_map.keys()
|
||||||
keys.sort()
|
keys.sort()
|
||||||
keys.reverse()
|
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)
|
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;
|
# 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://brandeins.de/'+url
|
||||||
|
|
||||||
# url = "http://www.brandeins.de/archiv/magazin/tierisch.html"
|
# 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':''})
|
current_articles.append({'title': title, 'url': url, 'description': description, 'date':''})
|
||||||
titles_and_articles.append([chapter_title, current_articles])
|
titles_and_articles.append([chapter_title, current_articles])
|
||||||
return titles_and_articles
|
return titles_and_articles
|
||||||
|
|
||||||
|
37
recipes/digizone.recipe
Normal file
37
recipes/digizone.recipe
Normal 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'])
|
||||||
|
]
|
@ -12,7 +12,6 @@ class AdvancedUserRecipe1301860159(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'en_EN'
|
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
|
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
|
||||||
remove_tags = [dict(name='a'),dict(name='hr')]
|
remove_tags = [dict(name='a'),dict(name='hr')]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
foxnews.com
|
foxnews.com
|
||||||
'''
|
'''
|
||||||
@ -23,6 +23,7 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
extra_css = """
|
extra_css = """
|
||||||
body{font-family: Arial,sans-serif }
|
body{font-family: Arial,sans-serif }
|
||||||
.caption{font-size: x-small}
|
.caption{font-size: x-small}
|
||||||
|
.author,.dateline{font-size: small}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
@ -34,12 +35,12 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
|
|
||||||
remove_attributes = ['xmlns','lang']
|
remove_attributes = ['xmlns','lang']
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags=[
|
||||||
dict(name=['object','embed','link','script','iframe','meta','base'])
|
dict(attrs={'class':['user-control','logo','ad-300x250','url-description']})
|
||||||
,dict(attrs={'class':['user-control','url-description','ad-context']})
|
,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'})
|
remove_tags_after =dict(attrs={'class':'url-description'})
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
@ -55,3 +56,24 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + 'print'
|
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
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = u'2010, Tomasz Dlugosz <tomek3d@gmail.com>'
|
__copyright__ = u'2010-2011, Tomasz Dlugosz <tomek3d@gmail.com>'
|
||||||
'''
|
'''
|
||||||
frazpc.pl
|
frazpc.pl
|
||||||
'''
|
'''
|
||||||
@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ]
|
feeds = [
|
||||||
|
(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'),
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})]
|
(u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly')
|
||||||
|
|
||||||
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' <a href="http://www.frazpc.pl/[^>]*?">(Skomentuj|Komentarz(e)?\([0-9]*\))</a> \|', lambda match: '')]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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' ]
|
remove_attributes = [ 'width', 'height' ]
|
||||||
|
53
recipes/gazeta-prawna-calibre-v1.recipe
Normal file
53
recipes/gazeta-prawna-calibre-v1.recipe
Normal 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
|
@ -16,7 +16,7 @@ class Jezebel(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
use_embedded_content = False
|
use_embedded_content = True
|
||||||
language = 'en'
|
language = 'en'
|
||||||
masthead_url = 'http://cache.gawkerassets.com/assets/jezebel.com/img/logo.png'
|
masthead_url = 'http://cache.gawkerassets.com/assets/jezebel.com/img/logo.png'
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
@ -32,13 +32,12 @@ class Jezebel(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['width','height']
|
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/vip?format=xml')]
|
||||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
|
||||||
remove_tags_before = dict(name='h1')
|
remove_tags = [
|
||||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
{'class': 'feedflare'},
|
||||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
]
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/full')]
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
@ -16,7 +16,7 @@ class Kotaku(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
use_embedded_content = False
|
use_embedded_content = True
|
||||||
language = 'en'
|
language = 'en'
|
||||||
masthead_url = 'http://cache.gawkerassets.com/assets/kotaku.com/img/logo.png'
|
masthead_url = 'http://cache.gawkerassets.com/assets/kotaku.com/img/logo.png'
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
@ -31,13 +31,12 @@ class Kotaku(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['width','height']
|
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/vip?format=xml')]
|
||||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
|
||||||
remove_tags_before = dict(name='h1')
|
remove_tags = [
|
||||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
{'class': 'feedflare'},
|
||||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
]
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/full')]
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
37
recipes/lupa.recipe
Normal file
37
recipes/lupa.recipe
Normal 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
37
recipes/mesec.recipe
Normal 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'])
|
||||||
|
]
|
@ -6,8 +6,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class NovinkyCZ(BasicNewsRecipe):
|
class NovinkyCZ(BasicNewsRecipe):
|
||||||
title = 'Novinky'
|
title = 'Novinky'
|
||||||
__author__ = 'Tomas Latal'
|
__author__ = 'Tomas Latal'
|
||||||
__version__ = '1.0'
|
__version__ = '1.1'
|
||||||
__date__ = '24 April 2011'
|
__date__ = '30 April 2011'
|
||||||
description = 'News from server Novinky.cz'
|
description = 'News from server Novinky.cz'
|
||||||
oldest_article = 1
|
oldest_article = 1
|
||||||
max_articles_per_feed = 10
|
max_articles_per_feed = 10
|
||||||
@ -18,6 +18,7 @@ class NovinkyCZ(BasicNewsRecipe):
|
|||||||
publication_type = 'newsportal'
|
publication_type = 'newsportal'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_javascript = 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}'
|
extra_css = 'p.acmDescription{font-style:italic;} p.acmAuthor{font-size:0.8em; color:#707070}'
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
|
37
recipes/podnikatel.recipe
Normal file
37
recipes/podnikatel.recipe
Normal 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'])
|
||||||
|
]
|
@ -18,7 +18,7 @@ class TelepolisNews(BasicNewsRecipe):
|
|||||||
recursion = 0
|
recursion = 0
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = "utf-8"
|
encoding = "utf-8"
|
||||||
language = 'de_AT'
|
language = 'de'
|
||||||
|
|
||||||
use_embedded_content =False
|
use_embedded_content =False
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
|
@ -10,6 +10,8 @@ import re
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class Time(BasicNewsRecipe):
|
class Time(BasicNewsRecipe):
|
||||||
|
recipe_disabled = ('This recipe has been disabled as TIME no longer'
|
||||||
|
' publish complete articles on the web.')
|
||||||
title = u'Time'
|
title = u'Time'
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
__author__ = 'Kovid Goyal and Sujata Raman'
|
||||||
description = 'Weekly magazine'
|
description = 'Weekly magazine'
|
||||||
|
@ -7,13 +7,11 @@ usatoday.com
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag
|
|
||||||
import re
|
|
||||||
|
|
||||||
class USAToday(BasicNewsRecipe):
|
class USAToday(BasicNewsRecipe):
|
||||||
|
|
||||||
title = 'USA Today'
|
title = 'USA Today'
|
||||||
__author__ = 'GRiker'
|
__author__ = 'Kovid Goyal'
|
||||||
oldest_article = 1
|
oldest_article = 1
|
||||||
timefmt = ''
|
timefmt = ''
|
||||||
max_articles_per_feed = 20
|
max_articles_per_feed = 20
|
||||||
@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe):
|
|||||||
margin-bottom: 0em; \
|
margin-bottom: 0em; \
|
||||||
font-size: smaller;}\n \
|
font-size: smaller;}\n \
|
||||||
.articleBody {text-align: left;}\n '
|
.articleBody {text-align: left;}\n '
|
||||||
conversion_options = { 'linearize_tables' : True }
|
|
||||||
#simultaneous_downloads = 1
|
#simultaneous_downloads = 1
|
||||||
feeds = [
|
feeds = [
|
||||||
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
|
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
|
||||||
@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe):
|
|||||||
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
|
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
|
||||||
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
|
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
|
||||||
]
|
]
|
||||||
keep_only_tags = [dict(attrs={'class':[
|
keep_only_tags = [dict(attrs={'class':'story'})]
|
||||||
'byLine',
|
remove_tags = [
|
||||||
'inside-copy',
|
dict(attrs={'class':[
|
||||||
'inside-head',
|
'share',
|
||||||
'inside-head2',
|
'reprints',
|
||||||
'item',
|
'inline-h3',
|
||||||
'item-block',
|
'info-extras',
|
||||||
'photo-container',
|
'ppy-outer',
|
||||||
]}),
|
'ppy-caption',
|
||||||
dict(id=[
|
'comments',
|
||||||
'applyMainStoryPhoto',
|
'jump',
|
||||||
'permalink',
|
'pagetools',
|
||||||
])]
|
'post-attributes',
|
||||||
|
'tags',
|
||||||
|
'bottom-tools',
|
||||||
|
'sponsoredlinks',
|
||||||
|
]}),
|
||||||
|
dict(id=['pluck']),
|
||||||
|
]
|
||||||
|
|
||||||
remove_tags = [dict(attrs={'class':[
|
|
||||||
'comments',
|
|
||||||
'jump',
|
|
||||||
'pagetools',
|
|
||||||
'post-attributes',
|
|
||||||
'tags',
|
|
||||||
]}),
|
|
||||||
dict(id=[])]
|
|
||||||
|
|
||||||
#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","‘",string)
|
|
||||||
|
|
||||||
# Replace rsquo (\x92)
|
|
||||||
fixed = re.sub("\x92","’",fixed)
|
|
||||||
|
|
||||||
# Replace ldquo (\x93)
|
|
||||||
fixed = re.sub("\x93","“",fixed)
|
|
||||||
|
|
||||||
# Replace rdquo (\x94)
|
|
||||||
fixed = re.sub("\x94","”",fixed)
|
|
||||||
|
|
||||||
# Replace ndash (\x96)
|
|
||||||
fixed = re.sub("\x96","–",fixed)
|
|
||||||
|
|
||||||
# Replace mdash (\x97)
|
|
||||||
fixed = re.sub("\x97","—",fixed)
|
|
||||||
|
|
||||||
return fixed
|
|
||||||
|
|
||||||
def get_masthead_url(self):
|
def get_masthead_url(self):
|
||||||
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
|
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
|
||||||
@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe):
|
|||||||
masthead = None
|
masthead = None
|
||||||
return masthead
|
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 '&'
|
|
||||||
massaged = re.sub("&","&", 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(","," ·", 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" ·",
|
|
||||||
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
39
recipes/vitalia.recipe
Normal 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'])
|
||||||
|
]
|
@ -23,6 +23,9 @@ wWinMain(HINSTANCE Inst, HINSTANCE PrevInst,
|
|||||||
ret = execute_python_entrypoint(BASENAME, MODULE, FUNCTION,
|
ret = execute_python_entrypoint(BASENAME, MODULE, FUNCTION,
|
||||||
stdout_redirect, stderr_redirect);
|
stdout_redirect, stderr_redirect);
|
||||||
|
|
||||||
|
if (stdout != NULL) fclose(stdout);
|
||||||
|
if (stderr != NULL) fclose(stderr);
|
||||||
|
|
||||||
DeleteFile(stdout_redirect);
|
DeleteFile(stdout_redirect);
|
||||||
DeleteFile(stderr_redirect);
|
DeleteFile(stderr_redirect);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = u'calibre'
|
__appname__ = u'calibre'
|
||||||
numeric_version = (0, 7, 58)
|
numeric_version = (0, 8, 0)
|
||||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
|
@ -607,6 +607,7 @@ class StoreBase(Plugin): # {{{
|
|||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
author = 'John Schember'
|
author = 'John Schember'
|
||||||
type = _('Store')
|
type = _('Store')
|
||||||
|
minimum_calibre_version = (0, 8, 0)
|
||||||
|
|
||||||
actual_plugin = None
|
actual_plugin = None
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
|
|||||||
from calibre.constants import numeric_version
|
from calibre.constants import numeric_version
|
||||||
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
|
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre.utils.config import test_eight_code
|
|
||||||
|
|
||||||
# To archive plugins {{{
|
# To archive plugins {{{
|
||||||
class HTML2ZIP(FileTypePlugin):
|
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.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
|
||||||
from calibre.devices.nook.driver import NOOK, NOOK_COLOR
|
from calibre.devices.nook.driver import NOOK, NOOK_COLOR
|
||||||
from calibre.devices.prs505.driver import PRS505
|
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.android.driver import ANDROID, S60
|
||||||
from calibre.devices.nokia.driver import N770, N810, E71X, E52
|
from calibre.devices.nokia.driver import N770, N810, E71X, E52
|
||||||
from calibre.devices.eslick.driver import ESLICK, EBK52
|
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.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
from calibre.devices.bambook.driver import BAMBOOK
|
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.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
|
||||||
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
|
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,
|
plugins = [HTML2ZIP, PML2PMLZ, TXT2TXTZ, ArchiveExtract, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
||||||
Epubcheck, ]
|
Epubcheck, ]
|
||||||
|
|
||||||
if test_eight_code:
|
|
||||||
# New metadata download plugins {{{
|
# New metadata download plugins {{{
|
||||||
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
||||||
from calibre.ebooks.metadata.sources.amazon import Amazon
|
from calibre.ebooks.metadata.sources.amazon import Amazon
|
||||||
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
||||||
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
|
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
|
||||||
from calibre.ebooks.metadata.sources.overdrive import OverDrive
|
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 += [
|
plugins += [
|
||||||
ComicInput,
|
ComicInput,
|
||||||
@ -755,6 +744,9 @@ plugins += [
|
|||||||
EEEREADER,
|
EEEREADER,
|
||||||
NEXTBOOK,
|
NEXTBOOK,
|
||||||
ITUNES,
|
ITUNES,
|
||||||
|
BOEYE_BEX,
|
||||||
|
BOEYE_BDX,
|
||||||
|
USER_DEFINED,
|
||||||
]
|
]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||||
x.__name__.endswith('MetadataReader')]
|
x.__name__.endswith('MetadataReader')]
|
||||||
@ -867,10 +859,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
|||||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
|
||||||
|
|
||||||
if test_eight_code:
|
|
||||||
plugins += [ActionStore]
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -1096,10 +1085,8 @@ class Misc(PreferencesPlugin):
|
|||||||
|
|
||||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||||
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
||||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
|
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
|
||||||
|
MetadataSources]
|
||||||
if test_eight_code:
|
|
||||||
plugins.append(MetadataSources)
|
|
||||||
|
|
||||||
#}}}
|
#}}}
|
||||||
|
|
||||||
|
@ -15,12 +15,11 @@ from calibre.customize.profiles import InputProfile, OutputProfile
|
|||||||
from calibre.customize.builtins import plugins as builtin_plugins
|
from calibre.customize.builtins import plugins as builtin_plugins
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.metadata.covers import CoverDownload
|
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
|
||||||
from calibre.ebooks.metadata.fetch import MetadataSource
|
plugin_dir, OptionParser)
|
||||||
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
|
||||||
plugin_dir, OptionParser, prefs
|
|
||||||
from calibre.ebooks.epub.fix import ePubFixer
|
from calibre.ebooks.epub.fix import ePubFixer
|
||||||
from calibre.ebooks.metadata.sources.base import Source
|
from calibre.ebooks.metadata.sources.base import Source
|
||||||
|
from calibre.constants import DEBUG
|
||||||
|
|
||||||
builtin_names = frozenset([p.name for p in builtin_plugins])
|
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
|
config['enabled_plugins'] = ep
|
||||||
|
|
||||||
default_disabled_plugins = set([
|
default_disabled_plugins = set([
|
||||||
'Douban Books', 'Douban.com covers', 'Nicebooks', 'Nicebooks covers',
|
'Overdrive',
|
||||||
'Kent District Library'
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def is_disabled(plugin):
|
def is_disabled(plugin):
|
||||||
@ -190,44 +188,6 @@ def output_profiles():
|
|||||||
yield plugin
|
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 # {{{
|
# Interface Actions # {{{
|
||||||
|
|
||||||
def interface_actions():
|
def interface_actions():
|
||||||
@ -527,8 +487,9 @@ def initialize_plugins():
|
|||||||
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
|
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
|
||||||
_initialized_plugins.append(plugin)
|
_initialized_plugins.append(plugin)
|
||||||
except:
|
except:
|
||||||
print 'Failed to initialize plugin...'
|
print 'Failed to initialize plugin:', repr(zfp)
|
||||||
traceback.print_exc()
|
if DEBUG:
|
||||||
|
traceback.print_exc()
|
||||||
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
|
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
|
||||||
reread_filetype_plugins()
|
reread_filetype_plugins()
|
||||||
reread_metadata_plugins()
|
reread_metadata_plugins()
|
||||||
|
@ -156,3 +156,60 @@ def debug(ioreg_to_tmp=False, buf=None):
|
|||||||
sys.stdout = oldo
|
sys.stdout = oldo
|
||||||
sys.stderr = olde
|
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
|
||||||
|
@ -62,7 +62,7 @@ class ANDROID(USBMS):
|
|||||||
0x502 : { 0x3203 : [0x0100]},
|
0x502 : { 0x3203 : [0x0100]},
|
||||||
|
|
||||||
# Dell
|
# Dell
|
||||||
0x413c : { 0xb007 : [0x0100, 0x0224]},
|
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
|
||||||
|
|
||||||
# LG
|
# LG
|
||||||
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
|
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
|
||||||
@ -112,7 +112,7 @@ class ANDROID(USBMS):
|
|||||||
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB']
|
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB']
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB']
|
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD']
|
||||||
|
|
||||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||||
|
|
||||||
|
@ -163,6 +163,8 @@ class ITUNES(DriverBase):
|
|||||||
settings()
|
settings()
|
||||||
set_progress_reporter()
|
set_progress_reporter()
|
||||||
upload_books()
|
upload_books()
|
||||||
|
_get_fpath()
|
||||||
|
_update_epub_metadata()
|
||||||
add_books_to_metadata()
|
add_books_to_metadata()
|
||||||
use_plugboard_ext()
|
use_plugboard_ext()
|
||||||
set_plugboard()
|
set_plugboard()
|
||||||
@ -504,7 +506,7 @@ class ITUNES(DriverBase):
|
|||||||
if self.iTunes:
|
if self.iTunes:
|
||||||
# Check for connected book-capable device
|
# Check for connected book-capable device
|
||||||
self.sources = self._get_sources()
|
self.sources = self._get_sources()
|
||||||
if 'iPod' in self.sources:
|
if 'iPod' in self.sources and not self.ejected:
|
||||||
#if DEBUG:
|
#if DEBUG:
|
||||||
#sys.stdout.write('.')
|
#sys.stdout.write('.')
|
||||||
#sys.stdout.flush()
|
#sys.stdout.flush()
|
||||||
@ -2034,16 +2036,17 @@ class ITUNES(DriverBase):
|
|||||||
if 'iPod' in self.sources:
|
if 'iPod' in self.sources:
|
||||||
connected_device = self.sources['iPod']
|
connected_device = self.sources['iPod']
|
||||||
device = self.iTunes.sources[connected_device]
|
device = self.iTunes.sources[connected_device]
|
||||||
|
dev_books = None
|
||||||
for pl in device.playlists():
|
for pl in device.playlists():
|
||||||
if pl.special_kind() == appscript.k.Books:
|
if pl.special_kind() == appscript.k.Books:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" Book playlist: '%s'" % (pl.name()))
|
self.log.info(" Book playlist: '%s'" % (pl.name()))
|
||||||
books = pl.file_tracks()
|
dev_books = pl.file_tracks()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.log.error(" book_playlist not found")
|
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
|
# This may need additional entries for international iTunes users
|
||||||
if book.kind() in self.Audiobooks:
|
if book.kind() in self.Audiobooks:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -2621,45 +2624,42 @@ class ITUNES(DriverBase):
|
|||||||
# Touch the OPF timestamp
|
# Touch the OPF timestamp
|
||||||
try:
|
try:
|
||||||
zf_opf = ZipFile(fpath,'r')
|
zf_opf = ZipFile(fpath,'r')
|
||||||
|
fnames = zf_opf.namelist()
|
||||||
|
opf = [x for x in fnames if '.opf' in x][0]
|
||||||
except:
|
except:
|
||||||
raise UserFeedback("'%s' is not a valid EPUB" % metadata.title,
|
raise UserFeedback("'%s' is not a valid EPUB" % metadata.title,
|
||||||
None,
|
None,
|
||||||
level=UserFeedback.WARN)
|
level=UserFeedback.WARN)
|
||||||
fnames = zf_opf.namelist()
|
|
||||||
try:
|
opf_tree = etree.fromstring(zf_opf.read(opf))
|
||||||
opf = [x for x in fnames if '.opf' in x][0]
|
md_els = opf_tree.xpath('.//*[local-name()="metadata"]')
|
||||||
except:
|
if md_els:
|
||||||
opf = None
|
ts = md_els[0].find('.//*[@name="calibre:timestamp"]')
|
||||||
if opf:
|
if ts is not None:
|
||||||
opf_tree = etree.fromstring(zf_opf.read(opf))
|
timestamp = ts.get('content')
|
||||||
md_els = opf_tree.xpath('.//*[local-name()="metadata"]')
|
old_ts = parse_date(timestamp)
|
||||||
if md_els:
|
metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
|
||||||
ts = md_els[0].find('.//*[@name="calibre:timestamp"]')
|
old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo)
|
||||||
if ts is not None:
|
if DEBUG:
|
||||||
timestamp = ts.get('content')
|
self.log.info(" existing timestamp: %s" % metadata.timestamp)
|
||||||
old_ts = parse_date(timestamp)
|
|
||||||
metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
|
|
||||||
old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo)
|
|
||||||
if DEBUG:
|
|
||||||
self.log.info(" existing timestamp: %s" % metadata.timestamp)
|
|
||||||
else:
|
|
||||||
metadata.timestamp = now()
|
|
||||||
if DEBUG:
|
|
||||||
self.log.info(" add timestamp: %s" % metadata.timestamp)
|
|
||||||
else:
|
else:
|
||||||
metadata.timestamp = now()
|
metadata.timestamp = now()
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.warning(" missing <metadata> block in OPF file")
|
|
||||||
self.log.info(" add timestamp: %s" % metadata.timestamp)
|
self.log.info(" add timestamp: %s" % metadata.timestamp)
|
||||||
# Force the language declaration for iBooks 1.1
|
else:
|
||||||
#metadata.language = get_lang().replace('_', '-')
|
metadata.timestamp = now()
|
||||||
|
|
||||||
# Updates from metadata plugboard (ignoring publisher)
|
|
||||||
metadata.language = metadata_x.language
|
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
if metadata.language != metadata_x.language:
|
self.log.warning(" missing <metadata> block in OPF file")
|
||||||
self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
|
self.log.info(" add timestamp: %s" % metadata.timestamp)
|
||||||
|
# Force the language declaration for iBooks 1.1
|
||||||
|
#metadata.language = get_lang().replace('_', '-')
|
||||||
|
|
||||||
|
# Updates from metadata plugboard (ignoring publisher)
|
||||||
|
metadata.language = metadata_x.language
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
if metadata.language != metadata_x.language:
|
||||||
|
self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
|
||||||
|
|
||||||
zf_opf.close()
|
zf_opf.close()
|
||||||
|
|
||||||
|
0
src/calibre/devices/boeye/__init__.py
Normal file
0
src/calibre/devices/boeye/__init__.py
Normal file
56
src/calibre/devices/boeye/driver.py
Normal file
56
src/calibre/devices/boeye/driver.py
Normal 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
|
@ -64,7 +64,7 @@ class HANLINV3(USBMS):
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
def linux_swap_drives(self, drives):
|
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)
|
drives = list(drives)
|
||||||
t = drives[0]
|
t = drives[0]
|
||||||
drives[0] = drives[1]
|
drives[0] = drives[1]
|
||||||
@ -95,7 +95,6 @@ class HANLINV5(HANLINV3):
|
|||||||
gui_name = 'Hanlin V5'
|
gui_name = 'Hanlin V5'
|
||||||
description = _('Communicate with Hanlin V5 eBook readers.')
|
description = _('Communicate with Hanlin V5 eBook readers.')
|
||||||
|
|
||||||
|
|
||||||
VENDOR_ID = [0x0492]
|
VENDOR_ID = [0x0492]
|
||||||
PRODUCT_ID = [0x8813]
|
PRODUCT_ID = [0x8813]
|
||||||
BCD = [0x319]
|
BCD = [0x319]
|
||||||
|
@ -164,7 +164,7 @@ class APNXBuilder(object):
|
|||||||
if c == '/':
|
if c == '/':
|
||||||
closing = True
|
closing = True
|
||||||
continue
|
continue
|
||||||
elif c in ('d', 'p'):
|
elif c == 'p':
|
||||||
if closing:
|
if closing:
|
||||||
in_p = False
|
in_p = False
|
||||||
else:
|
else:
|
||||||
|
@ -187,7 +187,7 @@ class LUMIREAD(USBMS):
|
|||||||
cfilepath = cfilepath.replace(os.sep+'books'+os.sep,
|
cfilepath = cfilepath.replace(os.sep+'books'+os.sep,
|
||||||
os.sep+'covers'+os.sep, 1)
|
os.sep+'covers'+os.sep, 1)
|
||||||
pdir = os.path.dirname(cfilepath)
|
pdir = os.path.dirname(cfilepath)
|
||||||
if not os.exists(pdir):
|
if not os.path.exists(pdir):
|
||||||
os.makedirs(pdir)
|
os.makedirs(pdir)
|
||||||
with open(cfilepath+'.jpg', 'wb') as f:
|
with open(cfilepath+'.jpg', 'wb') as f:
|
||||||
f.write(metadata.thumbnail[-1])
|
f.write(metadata.thumbnail[-1])
|
||||||
|
@ -94,6 +94,9 @@ class DeviceConfig(object):
|
|||||||
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
|
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
|
||||||
ec = []
|
ec = []
|
||||||
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
|
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'):
|
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
|
||||||
ec.append(config_widget.opt_extra_customization[i].isChecked())
|
ec.append(config_widget.opt_extra_customization[i].isChecked())
|
||||||
else:
|
else:
|
||||||
|
0
src/calibre/devices/user_defined/__init__.py
Normal file
0
src/calibre/devices/user_defined/__init__.py
Normal file
110
src/calibre/devices/user_defined/driver.py
Normal file
110
src/calibre/devices/user_defined/driver.py
Normal 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)
|
@ -7,10 +7,12 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import posixpath
|
||||||
|
|
||||||
from calibre import walk
|
from calibre import guess_type, walk
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
|
from calibre.ebooks.metadata.opf2 import OPF
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
|
||||||
class HTMLZInput(InputFormatPlugin):
|
class HTMLZInput(InputFormatPlugin):
|
||||||
@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin):
|
|||||||
|
|
||||||
# Extract content from zip archive.
|
# Extract content from zip archive.
|
||||||
zf = ZipFile(stream)
|
zf = ZipFile(stream)
|
||||||
zf.extractall('.')
|
zf.extractall()
|
||||||
|
|
||||||
for x in walk('.'):
|
for x in walk('.'):
|
||||||
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
|
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
|
||||||
@ -70,5 +72,24 @@ class HTMLZInput(InputFormatPlugin):
|
|||||||
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
|
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
|
||||||
mi = get_file_type_metadata(stream, file_ext)
|
mi = get_file_type_metadata(stream, file_ext)
|
||||||
meta_info_to_oeb_metadata(mi, oeb.metadata, log)
|
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
|
return oeb
|
||||||
|
@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from cStringIO import StringIO
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from calibre.customize.conversion import OutputFormatPlugin, \
|
from calibre.customize.conversion import OutputFormatPlugin, \
|
||||||
OptionRecommendation
|
OptionRecommendation
|
||||||
|
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
|
||||||
@ -79,10 +81,31 @@ class HTMLZOutput(OutputFormatPlugin):
|
|||||||
fname = os.path.join(tdir, 'images', images[item.href])
|
fname = os.path.join(tdir, 'images', images[item.href])
|
||||||
with open(fname, 'wb') as img:
|
with open(fname, 'wb') as img:
|
||||||
img.write(data)
|
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
|
# Metadata
|
||||||
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
|
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
|
||||||
mdataf.write(etree.tostring(oeb_book.metadata.to_opf1()))
|
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 = ZipFile(output_path, 'w')
|
||||||
htmlz.add_dir(tdir)
|
htmlz.add_dir(tdir)
|
||||||
|
@ -274,6 +274,9 @@ def check_isbn(isbn):
|
|||||||
if not isbn:
|
if not isbn:
|
||||||
return None
|
return None
|
||||||
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
|
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:
|
if len(isbn) == 10:
|
||||||
return check_isbn10(isbn)
|
return check_isbn10(isbn)
|
||||||
if len(isbn) == 13:
|
if len(isbn) == 13:
|
||||||
|
@ -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())
|
|
@ -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())
|
|
@ -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
|
|
@ -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())
|
|
@ -13,7 +13,7 @@ import posixpath
|
|||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
|
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
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.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.zipfile import ZipFile, safe_replace
|
from calibre.utils.zipfile import ZipFile, safe_replace
|
||||||
|
|
||||||
@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True):
|
|||||||
opf = OPF(opf_stream)
|
opf = OPF(opf_stream)
|
||||||
mi = opf.to_book_metadata()
|
mi = opf.to_book_metadata()
|
||||||
if extract_cover:
|
if extract_cover:
|
||||||
cover_name = opf.raster_cover
|
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
|
||||||
if cover_name:
|
if cover_href:
|
||||||
mi.cover_data = ('jpg', zf.read(cover_name))
|
mi.cover_data = ('jpg', zf.read(cover_href))
|
||||||
except:
|
except:
|
||||||
return mi
|
return mi
|
||||||
return mi
|
return mi
|
||||||
@ -59,17 +59,20 @@ def set_metadata(stream, mi):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
if new_cdata:
|
if new_cdata:
|
||||||
raster_cover = opf.raster_cover
|
cover = opf.cover
|
||||||
if not raster_cover:
|
if not cover:
|
||||||
raster_cover = 'cover.jpg'
|
cover = 'cover.jpg'
|
||||||
cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover)
|
cpath = posixpath.join(posixpath.dirname(opf_path), cover)
|
||||||
new_cover = _write_new_cover(new_cdata, cpath)
|
new_cover = _write_new_cover(new_cdata, cpath)
|
||||||
replacements[cpath] = open(new_cover.name, 'rb')
|
replacements[cpath] = open(new_cover.name, 'rb')
|
||||||
|
mi.cover = cover
|
||||||
|
|
||||||
# Update the metadata.
|
# 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())
|
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.
|
# Cleanup temporary files.
|
||||||
try:
|
try:
|
||||||
|
@ -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())
|
|
@ -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())
|
|
@ -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())
|
|
@ -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('&')])
|
|
||||||
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())
|
|
@ -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())
|
|
@ -966,7 +966,9 @@ class OPF(object): # {{{
|
|||||||
cover_id = covers[0].get('content')
|
cover_id = covers[0].get('content')
|
||||||
for item in self.itermanifest():
|
for item in self.itermanifest():
|
||||||
if item.get('id', None) == cover_id:
|
if item.get('id', None) == cover_id:
|
||||||
return item.get('href', None)
|
mt = item.get('media-type', '')
|
||||||
|
if 'xml' not in mt:
|
||||||
|
return item.get('href', None)
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def cover(self):
|
def cover(self):
|
||||||
|
@ -307,7 +307,7 @@ class Source(Plugin):
|
|||||||
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||||
[
|
[
|
||||||
# Remove things like: (2010) (Omnibus) etc.
|
# Remove things like: (2010) (Omnibus) etc.
|
||||||
(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
|
# Remove any strings that contain the substring edition inside
|
||||||
# parentheses
|
# parentheses
|
||||||
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
|
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
|
||||||
|
@ -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.base import create_log
|
||||||
from calibre.ebooks.metadata.sources.identify import identify
|
from calibre.ebooks.metadata.sources.identify import identify
|
||||||
from calibre.ebooks.metadata.sources.covers import download_cover
|
from calibre.ebooks.metadata.sources.covers import download_cover
|
||||||
from calibre.utils.config import test_eight_code
|
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
if not test_eight_code:
|
|
||||||
from calibre.ebooks.metadata.fetch import option_parser
|
|
||||||
return option_parser()
|
|
||||||
|
|
||||||
parser = OptionParser(textwrap.dedent(
|
parser = OptionParser(textwrap.dedent(
|
||||||
'''\
|
'''\
|
||||||
%prog [options]
|
%prog [options]
|
||||||
@ -48,9 +43,6 @@ def option_parser():
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
if not test_eight_code:
|
|
||||||
from calibre.ebooks.metadata.fetch import main
|
|
||||||
return main(args)
|
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
opts, args = parser.parse_args(args)
|
opts, args = parser.parse_args(args)
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from Queue import Queue, Empty
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
|
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
|
||||||
from calibre.ebooks.metadata.sources.base import create_log, msprefs
|
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
|
and plugin.get_cached_cover_url(result.identifiers) is not
|
||||||
None)
|
None)
|
||||||
result.identify_plugin = plugin
|
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 identify phase took %.2f seconds'%(time.time() - start_time))
|
||||||
log('The longest time (%f) was taken by:'%longest, lp)
|
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' %
|
log('We have %d merged results, merging took: %.2f seconds' %
|
||||||
(len(results), time.time() - start_time))
|
(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']
|
max_tags = msprefs['max_tags']
|
||||||
for r in results:
|
for r in results:
|
||||||
@ -459,6 +459,14 @@ def urls_from_identifiers(identifiers): # {{{
|
|||||||
if oclc:
|
if oclc:
|
||||||
ans.append(('OCLC', 'oclc', oclc,
|
ans.append(('OCLC', 'oclc', oclc,
|
||||||
'http://www.worldcat.org/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
|
return ans
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class OverDrive(Source):
|
|||||||
cached_cover_url_is_reliable = True
|
cached_cover_url_is_reliable = True
|
||||||
|
|
||||||
options = (
|
options = (
|
||||||
Option('get_full_metadata', 'bool', False,
|
Option('get_full_metadata', 'bool', True,
|
||||||
_('Download all metadata (slow)'),
|
_('Download all metadata (slow)'),
|
||||||
_('Enable this option to gather all metadata available from Overdrive.')),
|
_('Enable this option to gather all metadata available from Overdrive.')),
|
||||||
)
|
)
|
||||||
|
@ -253,6 +253,8 @@ class MobiReader(object):
|
|||||||
|
|
||||||
.italic { font-style: italic }
|
.italic { font-style: italic }
|
||||||
|
|
||||||
|
.underline { text-decoration: underline }
|
||||||
|
|
||||||
.mbp_pagebreak {
|
.mbp_pagebreak {
|
||||||
page-break-after: always; margin: 0; display: block
|
page-break-after: always; margin: 0; display: block
|
||||||
}
|
}
|
||||||
@ -601,6 +603,9 @@ class MobiReader(object):
|
|||||||
elif tag.tag == 'i':
|
elif tag.tag == 'i':
|
||||||
tag.tag = 'span'
|
tag.tag = 'span'
|
||||||
tag.attrib['class'] = 'italic'
|
tag.attrib['class'] = 'italic'
|
||||||
|
elif tag.tag == 'u':
|
||||||
|
tag.tag = 'span'
|
||||||
|
tag.attrib['class'] = 'underline'
|
||||||
elif tag.tag == 'b':
|
elif tag.tag == 'b':
|
||||||
tag.tag = 'span'
|
tag.tag = 'span'
|
||||||
tag.attrib['class'] = 'bold'
|
tag.attrib['class'] = 'bold'
|
||||||
|
@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Convert an ODT file into a Open Ebook
|
Convert an ODT file into a Open Ebook
|
||||||
'''
|
'''
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
from odf.odf2xhtml import ODF2XHTML
|
from odf.odf2xhtml import ODF2XHTML
|
||||||
|
|
||||||
from calibre import CurrentDir, walk
|
from calibre import CurrentDir, walk
|
||||||
@ -23,7 +25,51 @@ class Extract(ODF2XHTML):
|
|||||||
with open(name, 'wb') as f:
|
with open(name, 'wb') as f:
|
||||||
f.write(data)
|
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.utils.zipfile import ZipFile
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||||
@ -32,13 +78,17 @@ class Extract(ODF2XHTML):
|
|||||||
if not os.path.exists(odir):
|
if not os.path.exists(odir):
|
||||||
os.makedirs(odir)
|
os.makedirs(odir)
|
||||||
with CurrentDir(odir):
|
with CurrentDir(odir):
|
||||||
print 'Extracting ODT file...'
|
log('Extracting ODT file...')
|
||||||
html = self.odf2xhtml(stream)
|
html = self.odf2xhtml(stream)
|
||||||
# A blanket img specification like this causes problems
|
# 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
|
# an absolute height and width set that is larger than
|
||||||
# the available screen real estate
|
# the available screen real estate
|
||||||
html = html.replace('img { width: 100%; height: 100%; }', '')
|
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:
|
with open('index.xhtml', 'wb') as f:
|
||||||
f.write(html.encode('utf-8'))
|
f.write(html.encode('utf-8'))
|
||||||
zf = ZipFile(stream, 'r')
|
zf = ZipFile(stream, 'r')
|
||||||
@ -67,7 +117,7 @@ class ODTInput(InputFormatPlugin):
|
|||||||
|
|
||||||
def convert(self, stream, options, file_ext, log,
|
def convert(self, stream, options, file_ext, log,
|
||||||
accelerators):
|
accelerators):
|
||||||
return Extract()(stream, '.')
|
return Extract()(stream, '.', log)
|
||||||
|
|
||||||
def postprocess_book(self, oeb, opts, log):
|
def postprocess_book(self, oeb, opts, log):
|
||||||
# Fix <p><div> constructs as the asinine epubchecker complains
|
# Fix <p><div> constructs as the asinine epubchecker complains
|
||||||
|
@ -1049,8 +1049,8 @@ class Manifest(object):
|
|||||||
|
|
||||||
# Remove hyperlinks with no content as they cause rendering
|
# Remove hyperlinks with no content as they cause rendering
|
||||||
# artifacts in browser based renderers
|
# artifacts in browser based renderers
|
||||||
# Also remove empty <b> and <i> tags
|
# Also remove empty <b>, <u> and <i> tags
|
||||||
for a in xpath(data, '//h:a[@href]|//h:i|//h:b'):
|
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 \
|
if a.get('id', None) is None and a.get('name', None) is None \
|
||||||
and len(a) == 0 and not a.text:
|
and len(a) == 0 and not a.text:
|
||||||
remove_elem(a)
|
remove_elem(a)
|
||||||
|
@ -124,15 +124,20 @@ class Stylizer(object):
|
|||||||
|
|
||||||
def __init__(self, tree, path, oeb, opts, profile=None,
|
def __init__(self, tree, path, oeb, opts, profile=None,
|
||||||
extra_css='', user_css=''):
|
extra_css='', user_css=''):
|
||||||
from calibre.customize.ui import input_profiles
|
|
||||||
self.oeb, self.opts = oeb, opts
|
self.oeb, self.opts = oeb, opts
|
||||||
self.profile = None
|
self.profile = profile
|
||||||
for x in input_profiles():
|
|
||||||
if x.short_name == 'sony':
|
|
||||||
self.profile = x
|
|
||||||
break
|
|
||||||
if self.profile is None:
|
if self.profile is None:
|
||||||
self.profile = opts.input_profile
|
# 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:
|
||||||
|
# Just in case the default profile is removed in the future :)
|
||||||
|
self.profile = opts.output_profile
|
||||||
self.logger = oeb.logger
|
self.logger = oeb.logger
|
||||||
item = oeb.manifest.hrefs[path]
|
item = oeb.manifest.hrefs[path]
|
||||||
basename = os.path.basename(path)
|
basename = os.path.basename(path)
|
||||||
|
@ -36,7 +36,7 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
|
|||||||
m.clear('description')
|
m.clear('description')
|
||||||
m.add('description', mi.comments)
|
m.add('description', mi.comments)
|
||||||
elif override_input_metadata:
|
elif override_input_metadata:
|
||||||
m.clear('description')
|
m.clear('description')
|
||||||
if not mi.is_null('publisher'):
|
if not mi.is_null('publisher'):
|
||||||
m.clear('publisher')
|
m.clear('publisher')
|
||||||
m.add('publisher', mi.publisher)
|
m.add('publisher', mi.publisher)
|
||||||
|
@ -32,10 +32,11 @@ class PDFInput(InputFormatPlugin):
|
|||||||
|
|
||||||
def convert_new(self, stream, accelerators):
|
def convert_new(self, stream, accelerators):
|
||||||
from calibre.ebooks.pdf.reflow import PDFDocument
|
from calibre.ebooks.pdf.reflow import PDFDocument
|
||||||
|
from calibre.utils.cleantext import clean_ascii_chars
|
||||||
if pdfreflow_err:
|
if pdfreflow_err:
|
||||||
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
|
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
|
||||||
pdfreflow.reflow(stream.read(), 1, -1)
|
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)
|
PDFDocument(xml, self.opts, self.log)
|
||||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ import cStringIO
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre.utils.filenames import ascii_text
|
|
||||||
from calibre.utils.magick.draw import save_cover_data_to, identify_data
|
from calibre.utils.magick.draw import save_cover_data_to, identify_data
|
||||||
|
|
||||||
TAGS = {
|
TAGS = {
|
||||||
@ -79,8 +78,7 @@ def txt2rtf(text):
|
|||||||
elif val <= 127:
|
elif val <= 127:
|
||||||
buf.write(x)
|
buf.write(x)
|
||||||
else:
|
else:
|
||||||
repl = ascii_text(x)
|
c = r'\u{0:d}?'.format(val)
|
||||||
c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl))
|
|
||||||
buf.write(c)
|
buf.write(c)
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ if isosx:
|
|||||||
)
|
)
|
||||||
gprefs.defaults['action-layout-toolbar'] = (
|
gprefs.defaults['action-layout-toolbar'] = (
|
||||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
'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',
|
'Connect Share', None, 'Remove Books',
|
||||||
)
|
)
|
||||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||||
@ -48,7 +48,7 @@ else:
|
|||||||
gprefs.defaults['action-layout-menubar-device'] = ()
|
gprefs.defaults['action-layout-menubar-device'] = ()
|
||||||
gprefs.defaults['action-layout-toolbar'] = (
|
gprefs.defaults['action-layout-toolbar'] = (
|
||||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
'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',
|
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
|
||||||
)
|
)
|
||||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||||
|
@ -20,9 +20,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||||
from calibre.gui2.actions import InterfaceAction
|
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.ebooks.metadata import MetaInformation
|
||||||
from calibre.utils.config import test_eight_code
|
|
||||||
from calibre.ebooks.metadata.sources.base import msprefs
|
from calibre.ebooks.metadata.sources.base import msprefs
|
||||||
|
|
||||||
def get_filters():
|
def get_filters():
|
||||||
@ -180,26 +179,17 @@ class AddAction(InterfaceAction):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
|
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
|
||||||
self.isbn_add_dialog.accept()
|
self.isbn_add_dialog.accept()
|
||||||
if test_eight_code:
|
orig = msprefs['ignore_fields']
|
||||||
orig = msprefs['ignore_fields']
|
new = list(orig)
|
||||||
new = list(orig)
|
for x in ('title', 'authors'):
|
||||||
for x in ('title', 'authors'):
|
if x in new:
|
||||||
if x in new:
|
new.remove(x)
|
||||||
new.remove(x)
|
msprefs['ignore_fields'] = new
|
||||||
msprefs['ignore_fields'] = new
|
try:
|
||||||
try:
|
self.gui.iactions['Edit Metadata'].download_metadata(
|
||||||
self.gui.iactions['Edit Metadata'].download_metadata(
|
ids=self.add_by_isbn_ids)
|
||||||
ids=self.add_by_isbn_ids)
|
finally:
|
||||||
finally:
|
msprefs['ignore_fields'] = orig
|
||||||
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
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -246,7 +246,8 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
def delete_requested(self, name, location):
|
def delete_requested(self, name, location):
|
||||||
loc = location.replace('/', os.sep)
|
loc = location.replace('/', os.sep)
|
||||||
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
|
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,
|
'<b>permanently deleted</b>. Are you sure?') % loc,
|
||||||
show_copy_button=False):
|
show_copy_button=False):
|
||||||
return
|
return
|
||||||
|
@ -10,15 +10,13 @@ from functools import partial
|
|||||||
|
|
||||||
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
|
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog, config, Dispatcher, question_dialog
|
from calibre.gui2 import error_dialog, Dispatcher, question_dialog
|
||||||
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
|
||||||
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.config import test_eight_code
|
|
||||||
|
|
||||||
class EditMetadataAction(InterfaceAction):
|
class EditMetadataAction(InterfaceAction):
|
||||||
|
|
||||||
@ -36,22 +34,8 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
md.addAction(_('Edit metadata in bulk'),
|
md.addAction(_('Edit metadata in bulk'),
|
||||||
partial(self.edit_metadata, False, bulk=True))
|
partial(self.edit_metadata, False, bulk=True))
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
if test_eight_code:
|
md.addAction(_('Download metadata and covers'), self.download_metadata,
|
||||||
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,
|
|
||||||
Qt.ControlModifier+Qt.Key_D)
|
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
|
self.metadata_menu = md
|
||||||
|
|
||||||
mb = QMenu()
|
mb = QMenu()
|
||||||
@ -88,7 +72,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
_('No books selected'), show=True)
|
_('No books selected'), show=True)
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
ids = [db.id(row.row()) for row in rows]
|
ids = [db.id(row.row()) for row in rows]
|
||||||
from calibre.gui2.metadata.bulk_download2 import start_download
|
from calibre.gui2.metadata.bulk_download import start_download
|
||||||
start_download(self.gui, ids,
|
start_download(self.gui, ids,
|
||||||
Dispatcher(self.metadata_downloaded))
|
Dispatcher(self.metadata_downloaded))
|
||||||
|
|
||||||
@ -96,7 +80,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
if job.failed:
|
if job.failed:
|
||||||
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
|
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
|
||||||
return
|
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 = \
|
id_map, failed_ids, failed_covers, all_failed, det_msg = \
|
||||||
get_job_details(job)
|
get_job_details(job)
|
||||||
if all_failed:
|
if all_failed:
|
||||||
@ -112,8 +96,9 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
show_copy_button = False
|
show_copy_button = False
|
||||||
if failed_ids or failed_covers:
|
if failed_ids or failed_covers:
|
||||||
show_copy_button = True
|
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'
|
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)
|
payload = (id_map, failed_ids, failed_covers)
|
||||||
from calibre.gui2.dialogs.message_box import ProceedNotification
|
from calibre.gui2.dialogs.message_box import ProceedNotification
|
||||||
@ -158,49 +143,6 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
|
|
||||||
self.apply_metadata_changes(id_map)
|
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):
|
def edit_metadata(self, checked, bulk=None):
|
||||||
@ -227,9 +169,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
|
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
|
||||||
current_row = row_list.index(cr)
|
current_row = row_list.index(cr)
|
||||||
|
|
||||||
func = (self.do_edit_metadata if test_eight_code else
|
changed, rows_to_refresh = self.do_edit_metadata(row_list, current_row)
|
||||||
self.do_edit_metadata_old)
|
|
||||||
changed, rows_to_refresh = func(row_list, current_row)
|
|
||||||
|
|
||||||
m = self.gui.library_view.model()
|
m = self.gui.library_view.model()
|
||||||
|
|
||||||
@ -244,36 +184,6 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
m.current_changed(current, previous)
|
m.current_changed(current, previous)
|
||||||
self.gui.tags_view.recount()
|
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):
|
def do_edit_metadata(self, row_list, current_row):
|
||||||
from calibre.gui2.metadata.single import edit_metadata
|
from calibre.gui2.metadata.single import edit_metadata
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
@ -613,6 +523,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
self.applied_ids, cr)
|
self.applied_ids, cr)
|
||||||
if self.gui.cover_flow:
|
if self.gui.cover_flow:
|
||||||
self.gui.cover_flow.dataChanged()
|
self.gui.cover_flow.dataChanged()
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
|
||||||
self.apply_id_map = []
|
self.apply_id_map = []
|
||||||
self.apply_pd = None
|
self.apply_pd = None
|
||||||
|
@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
|
|||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.gui2.preferences.main import Preferences
|
from calibre.gui2.preferences.main import Preferences
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.constants import DEBUG
|
from calibre.constants import DEBUG, isosx
|
||||||
|
|
||||||
class PreferencesAction(InterfaceAction):
|
class PreferencesAction(InterfaceAction):
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction):
|
|||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
pm = QMenu()
|
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'),
|
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
|
||||||
self.gui.run_wizard)
|
self.gui.run_wizard)
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
|
@ -60,7 +60,7 @@ class ViewAction(InterfaceAction):
|
|||||||
|
|
||||||
def build_menus(self, db):
|
def build_menus(self, db):
|
||||||
self.view_menu.clear()
|
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.addAction(self.view_specific_action)
|
||||||
self.view_menu.addSeparator()
|
self.view_menu.addSeparator()
|
||||||
self.view_menu.addAction(self.action_pick_random)
|
self.view_menu.addAction(self.action_pick_random)
|
||||||
|
@ -62,8 +62,18 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
|
|
||||||
if isinstance(extra_customization_message, list):
|
if isinstance(extra_customization_message, list):
|
||||||
self.opt_extra_customization = []
|
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):
|
for i, m in enumerate(extra_customization_message):
|
||||||
label_text, tt = parse_msg(m)
|
label_text, tt = parse_msg(m)
|
||||||
|
if not label_text:
|
||||||
|
self.opt_extra_customization.append(None)
|
||||||
|
continue
|
||||||
if isinstance(settings.extra_customization[i], bool):
|
if isinstance(settings.extra_customization[i], bool):
|
||||||
self.opt_extra_customization.append(QCheckBox(label_text))
|
self.opt_extra_customization.append(QCheckBox(label_text))
|
||||||
self.opt_extra_customization[-1].setToolTip(tt)
|
self.opt_extra_customization[-1].setToolTip(tt)
|
||||||
@ -75,8 +85,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
l.setBuddy(self.opt_extra_customization[i])
|
l.setBuddy(self.opt_extra_customization[i])
|
||||||
l.setWordWrap(True)
|
l.setWordWrap(True)
|
||||||
self.opt_extra_customization[i].setText(settings.extra_customization[i])
|
self.opt_extra_customization[i].setText(settings.extra_customization[i])
|
||||||
self.extra_layout.addWidget(l)
|
self.extra_layout.addWidget(l, row_func(i, 0), col_func(i))
|
||||||
self.extra_layout.addWidget(self.opt_extra_customization[i])
|
self.extra_layout.addWidget(self.opt_extra_customization[i],
|
||||||
|
row_func(i, 1), col_func(i))
|
||||||
else:
|
else:
|
||||||
self.opt_extra_customization = QLineEdit()
|
self.opt_extra_customization = QLineEdit()
|
||||||
label_text, tt = parse_msg(extra_customization_message)
|
label_text, tt = parse_msg(extra_customization_message)
|
||||||
@ -86,8 +97,8 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
l.setWordWrap(True)
|
l.setWordWrap(True)
|
||||||
if settings.extra_customization:
|
if settings.extra_customization:
|
||||||
self.opt_extra_customization.setText(settings.extra_customization)
|
self.opt_extra_customization.setText(settings.extra_customization)
|
||||||
self.extra_layout.addWidget(l)
|
self.extra_layout.addWidget(l, 0, 0)
|
||||||
self.extra_layout.addWidget(self.opt_extra_customization)
|
self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
|
||||||
self.opt_save_template.setText(settings.save_template)
|
self.opt_save_template.setText(settings.save_template)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<layout class="QVBoxLayout" name="extra_layout"/>
|
<layout class="QGridLayout" name="extra_layout"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
|
@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__license__ = 'GPL v3'
|
__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.ebooks.metadata import author_to_author_sort
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
|
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):
|
class tableItem(QTableWidgetItem):
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
@ -19,7 +20,7 @@ class tableItem(QTableWidgetItem):
|
|||||||
|
|
||||||
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
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)
|
QDialog.__init__(self, parent)
|
||||||
Ui_EditAuthorsDialog.__init__(self)
|
Ui_EditAuthorsDialog.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -30,14 +31,23 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
|||||||
|
|
||||||
self.buttonBox.accepted.connect(self.accepted)
|
self.buttonBox.accepted.connect(self.accepted)
|
||||||
|
|
||||||
|
# Set up the column headings
|
||||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
self.table.setColumnCount(2)
|
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 = {}
|
self.authors = {}
|
||||||
auts = db.get_authors_with_ids()
|
auts = db.get_authors_with_ids()
|
||||||
self.table.setRowCount(len(auts))
|
self.table.setRowCount(len(auts))
|
||||||
setattr(self.table, '__lt__', lambda x, y: True if strcmp(x, y) < 0 else False)
|
|
||||||
select_item = None
|
select_item = None
|
||||||
for row, (id, author, sort) in enumerate(auts):
|
for row, (id, author, sort) in enumerate(auts):
|
||||||
author = author.replace('|', ',')
|
author = author.replace('|', ',')
|
||||||
@ -48,7 +58,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
|||||||
self.table.setItem(row, 0, aut)
|
self.table.setItem(row, 0, aut)
|
||||||
self.table.setItem(row, 1, sort)
|
self.table.setItem(row, 1, sort)
|
||||||
if id == id_to_select:
|
if id == id_to_select:
|
||||||
select_item = sort
|
if select_sort:
|
||||||
|
select_item = sort
|
||||||
|
else:
|
||||||
|
select_item = aut
|
||||||
self.table.resizeColumnsToContents()
|
self.table.resizeColumnsToContents()
|
||||||
|
|
||||||
# set up the cellChanged signal only after the table is filled
|
# 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.recalc_author_sort.clicked.connect(self.do_recalc_author_sort)
|
||||||
self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author)
|
self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author)
|
||||||
|
|
||||||
|
# Position on the desired item
|
||||||
if select_item is not None:
|
if select_item is not None:
|
||||||
self.table.setCurrentItem(select_item)
|
self.table.setCurrentItem(select_item)
|
||||||
self.table.editItem(select_item)
|
self.table.editItem(select_item)
|
||||||
|
self.start_find_pos = select_item.row() * 2 + select_item.column()
|
||||||
else:
|
else:
|
||||||
self.table.setCurrentCell(0, 0)
|
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):
|
def do_sort_by_author(self):
|
||||||
self.author_order = 1 if self.author_order == 0 else 0
|
self.author_order = 1 if self.author_order == 0 else 0
|
||||||
self.table.sortByColumn(0, self.author_order)
|
self.table.sortByColumn(0, self.author_order)
|
||||||
self.sort_by_author.setChecked(True)
|
self.sort_by_author.setChecked(True)
|
||||||
self.sort_by_author_sort.setChecked(False)
|
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):
|
def do_sort_by_author_sort(self):
|
||||||
self.author_sort_order = 1 if self.author_sort_order == 0 else 0
|
self.author_sort_order = 1 if self.author_sort_order == 0 else 0
|
||||||
self.table.sortByColumn(1, self.author_sort_order)
|
self.table.sortByColumn(1, self.author_sort_order)
|
||||||
self.sort_by_author.setChecked(False)
|
self.sort_by_author.setChecked(False)
|
||||||
self.sort_by_author_sort.setChecked(True)
|
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):
|
def accepted(self):
|
||||||
self.result = []
|
self.result = []
|
||||||
|
@ -20,6 +20,50 @@
|
|||||||
<string>Manage authors</string>
|
<string>Manage authors</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>&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&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>
|
<item>
|
||||||
<widget class="QTableWidget" name="table">
|
<widget class="QTableWidget" name="table">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -143,4 +187,11 @@ after changing Preferences->Advanced->Tweaks->Author sort name algorith
|
|||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
</connections>
|
</connections>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>HistoryLineEdit</class>
|
||||||
|
<extends>QComboBox</extends>
|
||||||
|
<header>calibre/gui2/widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -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)
|
|
||||||
|
|
@ -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><p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</a> 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>&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 &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
@ -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>&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>&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 &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>&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&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>&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>&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&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. <br><br>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>&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&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>&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&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>&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 &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>&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&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>&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&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>&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>&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>&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>
|
|
@ -7,16 +7,16 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, shutil
|
import os, shutil
|
||||||
from contextlib import closing
|
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog
|
from PyQt4.Qt import QDialog
|
||||||
|
|
||||||
from calibre.constants import isosx
|
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.gui2.dialogs.tweak_epub_ui import Ui_Dialog
|
||||||
from calibre.libunzip import extract as zipextract
|
from calibre.libunzip import extract as zipextract
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
||||||
|
PersistentTemporaryFile)
|
||||||
|
|
||||||
class TweakEpub(QDialog, Ui_Dialog):
|
class TweakEpub(QDialog, Ui_Dialog):
|
||||||
'''
|
'''
|
||||||
@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog):
|
|||||||
self.cancel_button.clicked.connect(self.reject)
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
self.explode_button.clicked.connect(self.explode)
|
self.explode_button.clicked.connect(self.explode)
|
||||||
self.rebuild_button.clicked.connect(self.rebuild)
|
self.rebuild_button.clicked.connect(self.rebuild)
|
||||||
|
self.preview_button.clicked.connect(self.preview)
|
||||||
|
|
||||||
# Position update dialog overlaying top left of app window
|
# Position update dialog overlaying top left of app window
|
||||||
parent_loc = parent.pos()
|
parent_loc = parent.pos()
|
||||||
self.move(parent_loc.x(),parent_loc.y())
|
self.move(parent_loc.x(),parent_loc.y())
|
||||||
|
|
||||||
|
self.gui = parent
|
||||||
|
self._preview_files = []
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
if isosx:
|
if isosx:
|
||||||
try:
|
try:
|
||||||
@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog):
|
|||||||
# Delete directory containing exploded ePub
|
# Delete directory containing exploded ePub
|
||||||
if self._exploded is not None:
|
if self._exploded is not None:
|
||||||
shutil.rmtree(self._exploded, ignore_errors=True)
|
shutil.rmtree(self._exploded, ignore_errors=True)
|
||||||
|
for x in self._preview_files:
|
||||||
|
try:
|
||||||
|
os.remove(x)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def display_exploded(self):
|
def display_exploded(self):
|
||||||
'''
|
'''
|
||||||
@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog):
|
|||||||
self.rebuild_button.setEnabled(True)
|
self.rebuild_button.setEnabled(True)
|
||||||
self.explode_button.setEnabled(False)
|
self.explode_button.setEnabled(False)
|
||||||
|
|
||||||
def rebuild(self, *args):
|
def do_rebuild(self, src):
|
||||||
self._output = os.path.join(self._exploded, 'rebuilt.epub')
|
with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf:
|
||||||
with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
|
|
||||||
# Write mimetype
|
# Write mimetype
|
||||||
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
|
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
|
||||||
# Write everything else
|
# Write everything else
|
||||||
@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog):
|
|||||||
zfn = os.path.relpath(absfn,
|
zfn = os.path.relpath(absfn,
|
||||||
self._exploded).replace(os.sep, '/')
|
self._exploded).replace(os.sep, '/')
|
||||||
zf.write(absfn, zfn)
|
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)
|
return QDialog.accept(self)
|
||||||
|
|
||||||
|
@ -23,6 +23,16 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string><p>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 <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p></string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QPushButton" name="explode_button">
|
<widget class="QPushButton" name="explode_button">
|
||||||
<property name="statusTip">
|
<property name="statusTip">
|
||||||
@ -37,23 +47,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>&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">
|
<item row="3" column="0">
|
||||||
<widget class="QPushButton" name="cancel_button">
|
<widget class="QPushButton" name="cancel_button">
|
||||||
<property name="statusTip">
|
<property name="statusTip">
|
||||||
@ -68,13 +61,31 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="3" column="1">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QPushButton" name="rebuild_button">
|
||||||
<property name="text">
|
<property name="enabled">
|
||||||
<string><p>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 <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p></string>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="statusTip">
|
||||||
<bool>true</bool>
|
<string>Rebuild ePub from exploded contents</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&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>&Preview ePub</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/view.png</normaloff>:/images/view.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{
|
|||||||
receiver = partial(self._location_selected, name)
|
receiver = partial(self._location_selected, name)
|
||||||
ac.triggered.connect(receiver)
|
ac.triggered.connect(receiver)
|
||||||
self.tooltips[name] = tooltip
|
self.tooltips[name] = tooltip
|
||||||
|
|
||||||
|
m = QMenu(parent)
|
||||||
|
self._mem.append(m)
|
||||||
|
a = m.addAction(icon, tooltip)
|
||||||
|
a.triggered.connect(receiver)
|
||||||
if name != 'library':
|
if name != 'library':
|
||||||
m = QMenu(parent)
|
|
||||||
self._mem.append(m)
|
|
||||||
a = m.addAction(icon, tooltip)
|
|
||||||
a.triggered.connect(receiver)
|
|
||||||
self._mem.append(a)
|
self._mem.append(a)
|
||||||
a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
|
a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
|
||||||
a.triggered.connect(self._eject_requested)
|
a.triggered.connect(self._eject_requested)
|
||||||
ac.setMenu(m)
|
|
||||||
self._mem.append(a)
|
self._mem.append(a)
|
||||||
else:
|
else:
|
||||||
ac.setToolTip(tooltip)
|
ac.setToolTip(tooltip)
|
||||||
|
ac.setMenu(m)
|
||||||
ac.calibre_name = name
|
ac.calibre_name = name
|
||||||
|
|
||||||
return ac
|
return ac
|
||||||
@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{
|
|||||||
|
|
||||||
def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
|
def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
|
||||||
switch_actions, choose_action):
|
switch_actions, choose_action):
|
||||||
self.switch_menu = QMenu()
|
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.switch_menu.addAction(choose_action)
|
||||||
self.cs_menus = []
|
self.cs_menus = []
|
||||||
for t, acs in [(_('Quick switch'), quick_actions),
|
for t, acs in [(_('Quick switch'), quick_actions),
|
||||||
@ -85,7 +91,9 @@ class LocationManager(QObject): # {{{
|
|||||||
self.switch_menu.addSeparator()
|
self.switch_menu.addSeparator()
|
||||||
for ac in switch_actions:
|
for ac in switch_actions:
|
||||||
self.switch_menu.addAction(ac)
|
self.switch_menu.addAction(ac)
|
||||||
self.library_action.setMenu(self.switch_menu)
|
|
||||||
|
if self.switch_menu != self.library_action.menu():
|
||||||
|
self.library_action.setMenu(self.switch_menu)
|
||||||
|
|
||||||
def _location_selected(self, location, *args):
|
def _location_selected(self, location, *args):
|
||||||
if location != self.current_location and hasattr(self,
|
if location != self.current_location and hasattr(self,
|
||||||
|
@ -439,10 +439,16 @@ class BooksView(QTableView): # {{{
|
|||||||
|
|
||||||
if tweaks['sort_columns_at_startup'] is not None:
|
if tweaks['sort_columns_at_startup'] is not None:
|
||||||
sh = []
|
sh = []
|
||||||
for c,d in tweaks['sort_columns_at_startup']:
|
try:
|
||||||
if not isinstance(d, bool):
|
for c,d in tweaks['sort_columns_at_startup']:
|
||||||
d = True if d == 0 else False
|
if not isinstance(d, bool):
|
||||||
sh.append((c, d))
|
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
|
old_state['sort_history'] = sh
|
||||||
|
|
||||||
self.apply_state(old_state)
|
self.apply_state(old_state)
|
||||||
|
@ -299,13 +299,13 @@ def run_gui(opts, args, actions, listener, app, gui_debug=None):
|
|||||||
if getattr(runner.main, 'debug_on_restart', False):
|
if getattr(runner.main, 'debug_on_restart', False):
|
||||||
run_in_debug_mode()
|
run_in_debug_mode()
|
||||||
else:
|
else:
|
||||||
|
import subprocess
|
||||||
print 'Restarting with:', e, sys.argv
|
print 'Restarting with:', e, sys.argv
|
||||||
if hasattr(sys, 'frameworks_dir'):
|
if hasattr(sys, 'frameworks_dir'):
|
||||||
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
|
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
|
||||||
import subprocess
|
|
||||||
subprocess.Popen('sleep 3s; open '+app, shell=True)
|
subprocess.Popen('sleep 3s; open '+app, shell=True)
|
||||||
else:
|
else:
|
||||||
os.execvp(e, sys.argv)
|
subprocess.Popen([e] + sys.argv[1:])
|
||||||
else:
|
else:
|
||||||
if iswindows:
|
if iswindows:
|
||||||
try:
|
try:
|
||||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import textwrap, re, os
|
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,
|
QIcon, QToolButton, QWidget, QLabel, QGridLayout,
|
||||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
|
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
|
||||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
|
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.icu import sort_key
|
||||||
from calibre.utils.config import tweaks, prefs
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.ebooks.metadata import (title_sort, authors_to_string,
|
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.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE,
|
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE,
|
||||||
choose_files, error_dialog, choose_images, question_dialog)
|
choose_files, error_dialog, choose_images)
|
||||||
from calibre.utils.date import local_tz, qt_to_dt
|
from calibre.utils.date import local_tz, qt_to_dt
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
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.gui2.comments_editor import Editor
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
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
|
The interface common to all widgets used to set basic metadata
|
||||||
@ -156,7 +166,7 @@ class AuthorsEdit(MultiCompleteComboBox):
|
|||||||
TOOLTIP = ''
|
TOOLTIP = ''
|
||||||
LABEL = _('&Author(s):')
|
LABEL = _('&Author(s):')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, manage_authors):
|
||||||
self.dialog = parent
|
self.dialog = parent
|
||||||
self.books_to_refresh = set([])
|
self.books_to_refresh = set([])
|
||||||
MultiCompleteComboBox.__init__(self, parent)
|
MultiCompleteComboBox.__init__(self, parent)
|
||||||
@ -164,6 +174,28 @@ class AuthorsEdit(MultiCompleteComboBox):
|
|||||||
self.setWhatsThis(self.TOOLTIP)
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
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):
|
def get_default(self):
|
||||||
return _('Unknown')
|
return _('Unknown')
|
||||||
@ -175,8 +207,8 @@ class AuthorsEdit(MultiCompleteComboBox):
|
|||||||
self.clear()
|
self.clear()
|
||||||
for i in all_authors:
|
for i in all_authors:
|
||||||
id, name = i
|
id, name = i
|
||||||
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
name = name.strip().replace('|', ',')
|
||||||
self.addItem(authors_to_string(name))
|
self.addItem(name)
|
||||||
|
|
||||||
self.set_separator('&')
|
self.set_separator('&')
|
||||||
self.set_space_before_sep(True)
|
self.set_space_before_sep(True)
|
||||||
@ -188,6 +220,8 @@ class AuthorsEdit(MultiCompleteComboBox):
|
|||||||
au = _('Unknown')
|
au = _('Unknown')
|
||||||
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||||
self.original_val = self.current_val
|
self.original_val = self.current_val
|
||||||
|
self.id_ = id_
|
||||||
|
self.db = db
|
||||||
|
|
||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
authors = self.current_val
|
authors = self.current_val
|
||||||
@ -238,7 +272,7 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
'No action is required if this is what you want.'))
|
'No action is required if this is what you want.'))
|
||||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
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)
|
self.textChanged.connect(self.update_state)
|
||||||
|
|
||||||
autogen_button.clicked.connect(self.auto_generate)
|
autogen_button.clicked.connect(self.auto_generate)
|
||||||
@ -260,12 +294,19 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
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):
|
def update_state(self, *args):
|
||||||
au = unicode(self.authors_edit.text())
|
au = unicode(self.authors_edit.text())
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
au = self.db.author_sort_from_authors(string_to_authors(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:
|
if normal:
|
||||||
col = 'rgb(0, 255, 0, 20%)'
|
col = 'rgb(0, 255, 0, 20%)'
|
||||||
else:
|
else:
|
||||||
@ -900,10 +941,13 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
|
|||||||
|
|
||||||
def edit(self, db, id_):
|
def edit(self, db, id_):
|
||||||
if self.changed:
|
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'
|
_('You have changed the tags. In order to use the tags'
|
||||||
' editor, you must either discard or apply these '
|
' 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_)
|
self.commit(db, id_)
|
||||||
db.commit()
|
db.commit()
|
||||||
self.original_val = self.current_val
|
self.original_val = self.current_val
|
||||||
|
@ -1,308 +1,195 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
# 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'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import traceback
|
|
||||||
from threading import Thread
|
|
||||||
from Queue import Queue, Empty
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from itertools import izip
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
from PyQt4.Qt import QObject, QTimer, QDialog, \
|
from PyQt4.Qt import (QIcon, QDialog,
|
||||||
QVBoxLayout, QTextBrowser, QLabel, QGroupBox, QDialogButtonBox
|
QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
|
||||||
|
|
||||||
from calibre.ebooks.metadata.fetch import search, get_social_metadata
|
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||||
from calibre.gui2 import config, error_dialog
|
from calibre.ebooks.metadata.sources.identify import identify, msprefs
|
||||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
from calibre.ebooks.metadata.sources.covers import download_cover
|
||||||
from calibre.ebooks.metadata.covers import download_cover
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.customize.ui import get_isbndb_key
|
from calibre.customize.ui import metadata_plugins
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
|
||||||
class Worker(Thread):
|
# Start download {{{
|
||||||
'Cover downloader'
|
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):
|
class ConfirmDialog(QDialog):
|
||||||
Thread.__init__(self)
|
|
||||||
self.daemon = True
|
|
||||||
self.jobs = Queue()
|
|
||||||
self.results = Queue()
|
|
||||||
|
|
||||||
def run(self):
|
def __init__(self, ids, parent):
|
||||||
while True:
|
QDialog.__init__(self, parent)
|
||||||
id, mi = self.jobs.get()
|
self.setWindowTitle(_('Schedule download?'))
|
||||||
if not getattr(mi, 'isbn', False):
|
self.setWindowIcon(QIcon(I('dialog_question.png')))
|
||||||
break
|
|
||||||
|
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:
|
try:
|
||||||
cdata, errors = download_cover(mi)
|
results = identify(log, Event(), title=title, authors=authors,
|
||||||
if cdata:
|
identifiers=identifiers)
|
||||||
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:
|
except:
|
||||||
self.results.put((id, mi, False, traceback.format_exc()))
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *args):
|
|
||||||
self.jobs.put((False, False))
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadMetadata(Thread):
|
|
||||||
'Metadata downloader'
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
self._run()
|
|
||||||
except Exception as e:
|
|
||||||
self.exception = e
|
|
||||||
self.tb = traceback.format_exc()
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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():
|
|
||||||
return
|
|
||||||
|
|
||||||
class DoDownload(QObject):
|
|
||||||
|
|
||||||
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 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 cancel(self, *args):
|
|
||||||
self.keep_going = False
|
|
||||||
self.downloader.keep_going = False
|
|
||||||
self.pd.reject()
|
|
||||||
|
|
||||||
def do_one(self):
|
|
||||||
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:
|
|
||||||
pass
|
pass
|
||||||
if not self.downloader.is_alive():
|
if results:
|
||||||
while True:
|
all_failed = False
|
||||||
try:
|
mi = merge_result(mi, results[0])
|
||||||
r = self.downloader.results.get_nowait()
|
identifiers = mi.identifiers
|
||||||
self.handle_result(r)
|
if not mi.is_null('rating'):
|
||||||
except Empty:
|
# set_metadata expects a rating out of 10
|
||||||
break
|
mi.rating *= 2
|
||||||
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()
|
|
||||||
else:
|
else:
|
||||||
try:
|
log.error('Failed to download metadata for', title)
|
||||||
self.set_metadata(id_)
|
failed_ids.add(i)
|
||||||
except:
|
# We don't want set_metadata operating on anything but covers
|
||||||
self.downloader.failures[id_] = \
|
mi = merge_result(mi, mi)
|
||||||
traceback.format_exc()
|
if covers:
|
||||||
|
cdata = download_cover(log, title=title, authors=authors,
|
||||||
def set_metadata(self, id_):
|
identifiers=identifiers)
|
||||||
mi = self.downloader.metadata[id_]
|
if cdata is not None:
|
||||||
if self.downloader.set_metadata:
|
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
|
||||||
self.db.set_metadata(id_, mi)
|
f.write(cdata[-1])
|
||||||
if not self.downloader.set_metadata and self.downloader.get_social_metadata:
|
mi.cover = f.name
|
||||||
if mi.rating:
|
all_failed = False
|
||||||
self.db.set_rating(id_, mi.rating)
|
else:
|
||||||
if mi.tags:
|
failed_covers.add(i)
|
||||||
self.db.set_tags(id_, mi.tags)
|
ans[i] = mi
|
||||||
if mi.comments:
|
count += 1
|
||||||
self.db.set_comment(id_, mi.comments)
|
notifications.put((count/len(ids),
|
||||||
if mi.series:
|
_('Downloaded %d of %d')%(count, len(ids))))
|
||||||
self.db.set_series(id_, mi.series)
|
log('Download complete, with %d failures'%len(failed_ids))
|
||||||
if mi.series_index is not None:
|
return (ans, failed_ids, failed_covers, title_map, all_failed)
|
||||||
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_()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,16 +103,18 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
||||||
|
|
||||||
self.deduce_author_sort_button = b = QToolButton(self)
|
self.deduce_author_sort_button = b = QToolButton(self)
|
||||||
b.setToolTip(_(
|
b.setToolTip('<p>' +
|
||||||
'Automatically create the author sort entry based on the current'
|
_('Automatically create the author sort entry based on the current '
|
||||||
' author entry.\n'
|
'author entry. Using this button to create author sort will '
|
||||||
'Using this button to create author sort will change author sort from'
|
'change author sort from red to green. There is a menu of '
|
||||||
' red to green.'))
|
'functions available under this button. Click and hold '
|
||||||
|
'on the button to see it.') + '</p>')
|
||||||
b.m = m = QMenu()
|
b.m = m = QMenu()
|
||||||
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
||||||
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
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)
|
b.setMenu(m)
|
||||||
self.authors = AuthorsEdit(self)
|
self.authors = AuthorsEdit(self, ac3)
|
||||||
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||||
ac2)
|
ac2)
|
||||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||||
@ -198,7 +200,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
ans = self.custom_metadata_widgets
|
ans = self.custom_metadata_widgets
|
||||||
for i in range(len(ans)-1):
|
for i in range(len(ans)-1):
|
||||||
if before is not None and i == 0:
|
if before is not None and i == 0:
|
||||||
pass# Do something
|
pass
|
||||||
if len(ans[i+1].widgets) == 2:
|
if len(ans[i+1].widgets) == 2:
|
||||||
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||||
else:
|
else:
|
||||||
@ -206,7 +208,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
for c in range(2, len(ans[i].widgets), 2):
|
for c in range(2, len(ans[i].widgets), 2):
|
||||||
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||||
if after is not None:
|
if after is not None:
|
||||||
pass # Do something
|
pass
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def do_view_format(self, path, fmt):
|
def do_view_format(self, path, fmt):
|
||||||
@ -290,13 +292,17 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
show=True)
|
show=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
def update_from_mi(self, mi):
|
def update_from_mi(self, mi, update_sorts=True):
|
||||||
if not mi.is_null('title'):
|
if not mi.is_null('title'):
|
||||||
self.title.current_val = mi.title
|
self.title.current_val = mi.title
|
||||||
|
if update_sorts:
|
||||||
|
self.title_sort.auto_generate()
|
||||||
if not mi.is_null('authors'):
|
if not mi.is_null('authors'):
|
||||||
self.authors.current_val = mi.authors
|
self.authors.current_val = mi.authors
|
||||||
if not mi.is_null('author_sort'):
|
if not mi.is_null('author_sort'):
|
||||||
self.author_sort.current_val = mi.author_sort
|
self.author_sort.current_val = mi.author_sort
|
||||||
|
elif update_sorts:
|
||||||
|
self.author_sort.auto_generate()
|
||||||
if not mi.is_null('rating'):
|
if not mi.is_null('rating'):
|
||||||
try:
|
try:
|
||||||
self.rating.current_val = mi.rating
|
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,
|
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
|
||||||
set_current_callback=None):
|
set_current_callback=None):
|
||||||
|
@ -19,7 +19,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
from calibre.ebooks.oeb.iterator import is_supported
|
from calibre.ebooks.oeb.iterator import is_supported
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.config import test_eight_code
|
|
||||||
|
|
||||||
class OutputFormatSetting(Setting):
|
class OutputFormatSetting(Setting):
|
||||||
|
|
||||||
@ -40,12 +39,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
r('network_timeout', prefs)
|
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('new_version_notification', config)
|
||||||
r('upload_news_to_device', config)
|
r('upload_news_to_device', config)
|
||||||
r('delete_news_from_library_on_upload', 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)
|
signal.connect(self.internally_viewed_formats_changed)
|
||||||
|
|
||||||
r('bools_are_tristate', db.prefs, restart_required=True)
|
r('bools_are_tristate', db.prefs, restart_required=True)
|
||||||
if test_eight_code:
|
r = self.register
|
||||||
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)
|
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):
|
def initialize(self):
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
|
@ -14,41 +14,14 @@
|
|||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="1">
|
<item row="1" column="0">
|
||||||
<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>&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 &social metadata (tags/ratings/etc.) by default</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_new_version_notification">
|
<widget class="QCheckBox" name="opt_new_version_notification">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show notification when &new version is available</string>
|
<string>Show notification when &new version is available</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="2">
|
<item row="1" column="1">
|
||||||
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
|
<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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="opt_upload_news_to_device">
|
<widget class="QCheckBox" name="opt_upload_news_to_device">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Automatically send downloaded &news to ebook reader</string>
|
<string>Automatically send downloaded &news to ebook reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="3" column="1">
|
||||||
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
|
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="5" column="0">
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_23">
|
<widget class="QLabel" name="label_23">
|
||||||
@ -97,7 +70,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="5" column="1">
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
@ -130,7 +103,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0">
|
<item row="7" column="0">
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="priority_label">
|
<widget class="QLabel" name="priority_label">
|
||||||
@ -169,7 +142,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="2">
|
<item row="7" column="1">
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_170">
|
<widget class="QLabel" name="label_170">
|
||||||
@ -202,7 +175,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0">
|
<item row="8" column="0">
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="edit_metadata_single_label">
|
<widget class="QLabel" name="edit_metadata_single_label">
|
||||||
@ -223,7 +196,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="20" column="0">
|
<item row="19" column="0">
|
||||||
<widget class="QGroupBox" name="groupBox_5">
|
<widget class="QGroupBox" name="groupBox_5">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Preferred &input format order:</string>
|
<string>Preferred &input format order:</string>
|
||||||
@ -285,7 +258,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="20" column="2">
|
<item row="19" column="1">
|
||||||
<widget class="QGroupBox" name="groupBox_3">
|
<widget class="QGroupBox" name="groupBox_3">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Use internal &viewer for:</string>
|
<string>Use internal &viewer for:</string>
|
||||||
@ -304,7 +277,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="2">
|
<item row="8" column="1">
|
||||||
<widget class="QPushButton" name="reset_confirmation_button">
|
<widget class="QPushButton" name="reset_confirmation_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Reset all disabled &confirmation dialogs</string>
|
<string>Reset all disabled &confirmation dialogs</string>
|
||||||
|
105
src/calibre/gui2/preferences/device_user_defined.py
Normal file
105
src/calibre/gui2/preferences/device_user_defined.py
Normal 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_()
|
@ -190,7 +190,15 @@ class FieldsModel(QAbstractListModel): # {{{
|
|||||||
return ans | Qt.ItemIsUserCheckable
|
return ans | Qt.ItemIsUserCheckable
|
||||||
|
|
||||||
def restore_defaults(self):
|
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()
|
self.reset()
|
||||||
|
|
||||||
def setData(self, index, val, role):
|
def setData(self, index, val, role):
|
||||||
@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.fields_view.setModel(self.fields_model)
|
self.fields_view.setModel(self.fields_model)
|
||||||
self.fields_model.dataChanged.connect(self.changed_signal)
|
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):
|
def configure_plugin(self):
|
||||||
for index in self.sources_view.selectionModel().selectedRows():
|
for index in self.sources_view.selectionModel().selectedRows():
|
||||||
plugin = self.sources_model.data(index, Qt.UserRole)
|
plugin = self.sources_model.data(index, Qt.UserRole)
|
||||||
|
@ -77,8 +77,8 @@
|
|||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Downloaded metadata fields</string>
|
<string>Downloaded metadata fields</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
<item>
|
<item row="0" column="0" colspan="2">
|
||||||
<widget class="QListView" name="fields_view">
|
<widget class="QListView" name="fields_view">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
|
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
|
||||||
@ -88,6 +88,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QPushButton" name="select_all_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Select all</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QPushButton" name="clear_all_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Clear all</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -30,6 +30,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('enforce_cpu_limit', config, restart_required=True)
|
r('enforce_cpu_limit', config, restart_required=True)
|
||||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||||
self.button_open_config_dir.clicked.connect(self.open_config_dir)
|
self.button_open_config_dir.clicked.connect(self.open_config_dir)
|
||||||
|
self.user_defined_device_button.clicked.connect(self.user_defined_device)
|
||||||
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
||||||
self.button_osx_symlinks.setVisible(isosx)
|
self.button_osx_symlinks.setVisible(isosx)
|
||||||
|
|
||||||
@ -38,6 +39,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
d = DebugDevice(self)
|
d = DebugDevice(self)
|
||||||
d.exec_()
|
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):
|
def open_config_dir(self, *args):
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
open_local_file(config_dir)
|
open_local_file(config_dir)
|
||||||
|
@ -58,7 +58,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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 &user defined device</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
<spacer name="verticalSpacer_6">
|
<spacer name="verticalSpacer_6">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
|
@ -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
|
|
@ -47,7 +47,7 @@ class StorePlugin(object): # {{{
|
|||||||
|
|
||||||
def __init__(self, gui, name):
|
def __init__(self, gui, name):
|
||||||
from calibre.gui2 import JSONConfig
|
from calibre.gui2 import JSONConfig
|
||||||
|
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.name = name
|
self.name = name
|
||||||
self.base_plugin = None
|
self.base_plugin = None
|
||||||
@ -79,14 +79,14 @@ class StorePlugin(object): # {{{
|
|||||||
return items as a generator.
|
return items as a generator.
|
||||||
|
|
||||||
Don't be lazy with the search! Load as much data as possible in the
|
Don't be lazy with the search! Load as much data as possible in the
|
||||||
:class:`calibre.gui2.store.search_result.SearchResult` object.
|
:class:`calibre.gui2.store.search_result.SearchResult` object.
|
||||||
However, if data (such as cover_url)
|
However, if data (such as cover_url)
|
||||||
isn't available because the store does not display cover images then it's okay to
|
isn't available because the store does not display cover images then it's okay to
|
||||||
ignore it.
|
ignore it.
|
||||||
|
|
||||||
At the very least a :class:`calibre.gui2.store.search_result.SearchResult`
|
At the very least a :class:`calibre.gui2.store.search_result.SearchResult`
|
||||||
returned by this function must have the title, author and id.
|
returned by this function must have the title, author and id.
|
||||||
|
|
||||||
If you have to parse multiple pages to get all of the data then implement
|
If you have to parse multiple pages to get all of the data then implement
|
||||||
:meth:`get_deatils` for retrieving additional information.
|
:meth:`get_deatils` for retrieving additional information.
|
||||||
|
|
||||||
@ -105,24 +105,24 @@ class StorePlugin(object): # {{{
|
|||||||
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_details(self, search_result, timeout=60):
|
def get_details(self, search_result, timeout=60):
|
||||||
'''
|
'''
|
||||||
Delayed search for information about specific search items.
|
Delayed search for information about specific search items.
|
||||||
|
|
||||||
Typically, this will be used when certain information such as
|
Typically, this will be used when certain information such as
|
||||||
formats, drm status, cover url are not part of the main search
|
formats, drm status, cover url are not part of the main search
|
||||||
results and the information is on another web page.
|
results and the information is on another web page.
|
||||||
|
|
||||||
Using this function allows for the main information (title, author)
|
Using this function allows for the main information (title, author)
|
||||||
to be displayed in the search results while other information can
|
to be displayed in the search results while other information can
|
||||||
take extra time to load. Splitting retrieving data that takes longer
|
take extra time to load. Splitting retrieving data that takes longer
|
||||||
to load into a separate function will give the illusion of the search
|
to load into a separate function will give the illusion of the search
|
||||||
being faster.
|
being faster.
|
||||||
|
|
||||||
:param search_result: A search result that need details set.
|
:param search_result: A search result that need details set.
|
||||||
:param timeout: The maximum amount of time in seconds to spend downloading details.
|
:param timeout: The maximum amount of time in seconds to spend downloading details.
|
||||||
|
|
||||||
:return: True if the search_result was modified otherwise False
|
:return: True if the search_result was modified otherwise False
|
||||||
'''
|
'''
|
||||||
return False
|
return False
|
||||||
@ -133,30 +133,30 @@ class StorePlugin(object): # {{{
|
|||||||
is called to update the caches. It is recommended to call this function
|
is called to update the caches. It is recommended to call this function
|
||||||
from :meth:`open`. Especially if :meth:`open` does anything other than
|
from :meth:`open`. Especially if :meth:`open` does anything other than
|
||||||
open a web page.
|
open a web page.
|
||||||
|
|
||||||
This function can be called at any time. It is up to the plugin to determine
|
This function can be called at any time. It is up to the plugin to determine
|
||||||
if the cache really does need updating. Unless :param:`force` is True, then
|
if the cache really does need updating. Unless :param:`force` is True, then
|
||||||
the plugin must update the cache. The only time force should be True is if
|
the plugin must update the cache. The only time force should be True is if
|
||||||
this function is called by the plugin's configuration dialog.
|
this function is called by the plugin's configuration dialog.
|
||||||
|
|
||||||
if :param:`suppress_progress` is False it is safe to assume that this function
|
if :param:`suppress_progress` is False it is safe to assume that this function
|
||||||
is being called from the main GUI thread so it is safe and recommended to use
|
is being called from the main GUI thread so it is safe and recommended to use
|
||||||
a QProgressDialog to display what is happening and allow the user to cancel
|
a QProgressDialog to display what is happening and allow the user to cancel
|
||||||
the operation. if :param:`suppress_progress` is True then run the update
|
the operation. if :param:`suppress_progress` is True then run the update
|
||||||
silently. In this case there is no guarantee what thread is calling this
|
silently. In this case there is no guarantee what thread is calling this
|
||||||
function so no Qt related functionality that requires being run in the main
|
function so no Qt related functionality that requires being run in the main
|
||||||
GUI thread should be run. E.G. Open a QProgressDialog.
|
GUI thread should be run. E.G. Open a QProgressDialog.
|
||||||
|
|
||||||
:param parent: The parent object to be used by an GUI dialogs.
|
:param parent: The parent object to be used by an GUI dialogs.
|
||||||
|
|
||||||
:param timeout: The maximum amount of time that should be spent in
|
:param timeout: The maximum amount of time that should be spent in
|
||||||
any given network connection.
|
any given network connection.
|
||||||
|
|
||||||
:param force: Force updating the cache even if the plugin has determined
|
:param force: Force updating the cache even if the plugin has determined
|
||||||
it is not necessary.
|
it is not necessary.
|
||||||
|
|
||||||
:param suppress_progress: Should a progress indicator be shown.
|
:param suppress_progress: Should a progress indicator be shown.
|
||||||
|
|
||||||
:return: True if the cache was updated, False otherwise.
|
:return: True if the cache was updated, False otherwise.
|
||||||
'''
|
'''
|
||||||
return False
|
return False
|
||||||
|
@ -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['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_col'] = self.results_view.model().sort_col
|
||||||
self.config['sort_order'] = self.results_view.model().sort_order
|
self.config['sort_order'] = self.results_view.model().sort_order
|
||||||
|
self.config['open_external'] = self.open_external.isChecked()
|
||||||
|
|
||||||
store_check = {}
|
store_check = {}
|
||||||
for n in self.store_plugins:
|
for n in self.store_plugins:
|
||||||
@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
else:
|
else:
|
||||||
self.resize_columns()
|
self.resize_columns()
|
||||||
|
|
||||||
|
self.open_external.setChecked(self.config.get('open_external', False))
|
||||||
|
|
||||||
store_check = self.config.get('store_checked', None)
|
store_check = self.config.get('store_checked', None)
|
||||||
if store_check:
|
if store_check:
|
||||||
for n in store_check:
|
for n in store_check:
|
||||||
@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
|
|
||||||
def open_store(self, index):
|
def open_store(self, index):
|
||||||
result = self.results_view.model().get_result(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):
|
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():
|
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():
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>215</width>
|
<width>215</width>
|
||||||
<height>116</height>
|
<height>93</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -101,6 +101,16 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</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 &external browser</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QSplitter" name="splitter_2">
|
<widget class="QSplitter" name="splitter_2">
|
||||||
|
@ -2048,12 +2048,12 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.library_view.select_rows(ids)
|
self.library_view.select_rows(ids)
|
||||||
# refreshing the tags view happens at the emit()/call() site
|
# 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
|
Open the manage authors dialog
|
||||||
'''
|
'''
|
||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
editor = EditAuthorsDialog(parent, db, id)
|
editor = EditAuthorsDialog(parent, db, id, select_sort)
|
||||||
d = editor.exec_()
|
d = editor.exec_()
|
||||||
if d:
|
if d:
|
||||||
for (id, old_author, new_author, new_sort) in editor.result:
|
for (id, old_author, new_author, new_sort) in editor.result:
|
||||||
|
@ -8,6 +8,7 @@ from collections import namedtuple
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from types import StringType, UnicodeType
|
||||||
|
|
||||||
from calibre import prints, prepare_string_for_xml, strftime
|
from calibre import prints, prepare_string_for_xml, strftime
|
||||||
from calibre.constants import preferred_encoding, DEBUG
|
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.customize.conversion import OptionRecommendation, DummyReporter
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
|
||||||
from calibre.ebooks.chardet import substitute_entites
|
from calibre.ebooks.chardet import substitute_entites
|
||||||
|
from calibre.library.save_to_disk import preprocess_template
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
from calibre.utils.bibtex import BibTeX
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf
|
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf
|
||||||
|
from calibre.utils.html2text import html2text
|
||||||
from calibre.utils.icu import capitalize
|
from calibre.utils.icu import capitalize
|
||||||
from calibre.utils.logging import default_log as log
|
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.magick.draw import thumbnail
|
||||||
|
from calibre.utils.zipfile import ZipFile, ZipInfo
|
||||||
|
|
||||||
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
|
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
|
||||||
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
|
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
|
||||||
@ -303,12 +307,6 @@ class BIBTEX(CatalogPlugin): # {{{
|
|||||||
|
|
||||||
def run(self, path_to_output, opts, db, notification=DummyReporter()):
|
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,
|
def create_bibtex_entry(entry, fields, mode, template_citation,
|
||||||
bibtexdict, citation_bibtex=True, calibre_files=True):
|
bibtexdict, citation_bibtex=True, calibre_files=True):
|
||||||
|
|
||||||
@ -365,6 +363,11 @@ class BIBTEX(CatalogPlugin): # {{{
|
|||||||
#\n removal
|
#\n removal
|
||||||
item = item.replace(u'\r\n',u' ')
|
item = item.replace(u'\r\n',u' ')
|
||||||
item = item.replace(u'\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))
|
bibtex_entry.append(u'note = "%s"' % bibtexdict.utf8ToBibtex(item))
|
||||||
|
|
||||||
elif field == 'isbn' :
|
elif field == 'isbn' :
|
||||||
@ -941,6 +944,7 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
catalog.createDirectoryStructure()
|
catalog.createDirectoryStructure()
|
||||||
catalog.copyResources()
|
catalog.copyResources()
|
||||||
catalog.buildSources()
|
catalog.buildSources()
|
||||||
|
Options managed in gui2.catalog.catalog_epub_mobi.py
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# A single number creates 'Last x days' only.
|
# A single number creates 'Last x days' only.
|
||||||
|
@ -33,7 +33,7 @@ from calibre import isbytestring
|
|||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
||||||
from calibre.utils.config import prefs, tweaks, from_json, to_json
|
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.utils.search_query_parser import saved_searches, set_saved_searches
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||||
from calibre.utils.magick.draw import save_cover_data_to
|
from calibre.utils.magick.draw import save_cover_data_to
|
||||||
@ -1920,6 +1920,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
result.append(r)
|
result.append(r)
|
||||||
return ' & '.join(result).replace('|', ',')
|
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):
|
def _set_authors(self, id, authors, allow_case_change=False):
|
||||||
if not authors:
|
if not authors:
|
||||||
authors = [_('Unknown')]
|
authors = [_('Unknown')]
|
||||||
@ -1933,14 +1945,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
a = a.strip().replace(',', '|')
|
a = a.strip().replace(',', '|')
|
||||||
if not isinstance(a, unicode):
|
if not isinstance(a, unicode):
|
||||||
a = a.decode(preferred_encoding, 'replace')
|
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:
|
if aus:
|
||||||
aid, name = aus[0]
|
aid, name, sort = aus[0]
|
||||||
# Handle change of case
|
# Handle change of case
|
||||||
if name != a:
|
if name != a:
|
||||||
if allow_case_change:
|
if allow_case_change:
|
||||||
self.conn.execute('''UPDATE authors
|
ns = author_to_author_sort(a.replace('|', ','))
|
||||||
SET name=? WHERE id=?''', (a, aid))
|
if strcmp(sort, ns) == 0:
|
||||||
|
sort = ns
|
||||||
|
self.conn.execute('''UPDATE authors SET name=?, sort=?
|
||||||
|
WHERE id=?''', (a, sort, aid))
|
||||||
case_change = True
|
case_change = True
|
||||||
else:
|
else:
|
||||||
a = name
|
a = name
|
||||||
@ -1957,17 +1972,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
bks = self.conn.get('''SELECT book FROM books_authors_link
|
bks = self.conn.get('''SELECT book FROM books_authors_link
|
||||||
WHERE author=?''', (aid,))
|
WHERE author=?''', (aid,))
|
||||||
books_to_refresh |= set([bk[0] for bk in bks])
|
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)
|
||||||
|
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)
|
ss = self.author_sort_from_book(id, index_is_id=True)
|
||||||
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
|
self._update_author_in_cache(id, ss, final_authors)
|
||||||
(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)
|
|
||||||
return books_to_refresh
|
return books_to_refresh
|
||||||
|
|
||||||
def set_authors(self, id, authors, notify=True, commit=True,
|
def set_authors(self, id, authors, notify=True, commit=True,
|
||||||
@ -2273,6 +2285,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return []
|
return []
|
||||||
return result
|
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):
|
def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
|
||||||
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
|
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
|
||||||
(new_sort.strip(), old_id))
|
(new_sort.strip(), old_id))
|
||||||
|
@ -100,7 +100,9 @@ Device Integration
|
|||||||
|
|
||||||
What devices does |app| support?
|
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.
|
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|?
|
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.
|
* 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>`_.
|
* 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?
|
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>`_.
|
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?
|
|app| is not starting on OS X?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -545,7 +564,7 @@ You have two choices:
|
|||||||
|
|
||||||
How is |app| licensed?
|
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?
|
How do I run calibre from my USB stick?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -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:
|
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 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.
|
* 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 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.
|
* 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
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user