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:
|
||||
# - title:
|
||||
|
||||
- version: 0.8.0
|
||||
date: 2010-05-06
|
||||
|
||||
new features:
|
||||
- title: "Go to http://calibre-ebook.com/new-in/eight to see what's new in 0.8.0"
|
||||
type: major
|
||||
|
||||
- version: 0.7.59
|
||||
date: 2011-04-30
|
||||
|
||||
bug fixes:
|
||||
- title: "Fixes a bug in 0.7.58 that caused too small fonts when converting to MOBI for the Kindle. Apologies."
|
||||
|
||||
- title: "Apple driver: Handle invalid EPUBs that do not contain an OPF file"
|
||||
|
||||
new recipes:
|
||||
- title: The Big Picture and Auto industry news
|
||||
author: welovelucy
|
||||
|
||||
- title: Gazeta Prawna
|
||||
author: Vroo
|
||||
|
||||
- title: Various Czech news sources
|
||||
author: Tomas Latal
|
||||
|
||||
- title: Diario de Ibiza
|
||||
author: Joan Tur
|
||||
|
||||
- version: 0.7.58
|
||||
date: 2011-04-29
|
||||
|
||||
|
16
recipes/auto_blog.recipe
Normal file
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'
|
||||
__copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>'
|
||||
__version__ = '0.98' # 2011-04-10
|
||||
__version__ = '0.98'
|
||||
|
||||
''' http://brandeins.de - Wirtschaftsmagazin '''
|
||||
import re
|
||||
import string
|
||||
@ -13,8 +14,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
class BrandEins(BasicNewsRecipe):
|
||||
|
||||
title = u'brand eins'
|
||||
__author__ = 'Constantin Hofstetter; Steffen Siebert'
|
||||
description = u'Wirtschaftsmagazin: Gets the last full issue on default. Set a integer value for the username-field to get older issues: 1 -> the newest (but not complete) issue, 2 -> the last complete issue (default), 3 -> the issue before 2 etc.'
|
||||
__author__ = 'Constantin Hofstetter'
|
||||
description = u'Wirtschaftsmagazin'
|
||||
publisher ='brandeins.de'
|
||||
category = 'politics, business, wirtschaft, Germany'
|
||||
use_embedded_content = False
|
||||
@ -105,10 +106,11 @@ class BrandEins(BasicNewsRecipe):
|
||||
keys = issue_map.keys()
|
||||
keys.sort()
|
||||
keys.reverse()
|
||||
selected_issue = issue_map[keys[issue-1]]
|
||||
selected_issue_key = keys[issue - 1]
|
||||
selected_issue = issue_map[selected_issue_key]
|
||||
url = selected_issue.get('href', False)
|
||||
# Get the title for the magazin - build it out of the title of the cover - take the issue and year;
|
||||
self.title = "brand eins "+ re.search(r"(?P<date>\d\d\/\d\d\d\d)", selected_issue.find('img').get('title', False)).group('date')
|
||||
self.title = "brand eins " + selected_issue_key[4:] + "/" + selected_issue_key[0:4]
|
||||
url = 'http://brandeins.de/'+url
|
||||
|
||||
# url = "http://www.brandeins.de/archiv/magazin/tierisch.html"
|
||||
@ -161,3 +163,4 @@ class BrandEins(BasicNewsRecipe):
|
||||
current_articles.append({'title': title, 'url': url, 'description': description, 'date':''})
|
||||
titles_and_articles.append([chapter_title, current_articles])
|
||||
return titles_and_articles
|
||||
|
||||
|
37
recipes/digizone.recipe
Normal file
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
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
language = 'en_EN'
|
||||
remove_javascript = True
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
|
||||
remove_tags = [dict(name='a'),dict(name='hr')]
|
||||
|
@ -1,5 +1,5 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
foxnews.com
|
||||
'''
|
||||
@ -23,6 +23,7 @@ class FoxNews(BasicNewsRecipe):
|
||||
extra_css = """
|
||||
body{font-family: Arial,sans-serif }
|
||||
.caption{font-size: x-small}
|
||||
.author,.dateline{font-size: small}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
@ -34,12 +35,12 @@ class FoxNews(BasicNewsRecipe):
|
||||
|
||||
remove_attributes = ['xmlns','lang']
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['object','embed','link','script','iframe','meta','base'])
|
||||
,dict(attrs={'class':['user-control','url-description','ad-context']})
|
||||
]
|
||||
remove_tags=[
|
||||
dict(attrs={'class':['user-control','logo','ad-300x250','url-description']})
|
||||
,dict(name=['meta','base','link','iframe','object','embed'])
|
||||
]
|
||||
|
||||
remove_tags_before=dict(name='h1')
|
||||
keep_only_tags=[dict(attrs={'id':'article-print'})]
|
||||
remove_tags_after =dict(attrs={'class':'url-description'})
|
||||
|
||||
feeds = [
|
||||
@ -55,3 +56,24 @@ class FoxNews(BasicNewsRecipe):
|
||||
|
||||
def print_version(self, url):
|
||||
return url + 'print'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('a'):
|
||||
limg = item.find('img')
|
||||
if item.string is not None:
|
||||
str = item.string
|
||||
item.replaceWith(str)
|
||||
else:
|
||||
if limg:
|
||||
item.name = 'div'
|
||||
item.attrs = []
|
||||
else:
|
||||
str = self.tag_to_string(item)
|
||||
item.replaceWith(str)
|
||||
for item in soup.findAll('img'):
|
||||
if not item.has_key('alt'):
|
||||
item['alt'] = 'image'
|
||||
return soup
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = u'2010, Tomasz Dlugosz <tomek3d@gmail.com>'
|
||||
__copyright__ = u'2010-2011, Tomasz Dlugosz <tomek3d@gmail.com>'
|
||||
'''
|
||||
frazpc.pl
|
||||
'''
|
||||
@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
|
||||
feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ]
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})]
|
||||
|
||||
remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})]
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
|
||||
[(r'<div id="post-[0-9]*"', lambda match: '<div id="FRAZ_CONTENT"'),
|
||||
(r'href="/f/news/', lambda match: 'href="http://www.frazpc.pl/f/news/'),
|
||||
(r' <a href="http://www.frazpc.pl/[^>]*?">(Skomentuj|Komentarz(e)?\([0-9]*\))</a> \|', lambda match: '')]
|
||||
feeds = [
|
||||
(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'),
|
||||
(u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly')
|
||||
]
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':'title-wrapper'}),
|
||||
dict(name='p', attrs={'class':'tags'}),
|
||||
dict(name='p', attrs={'class':'article-links'}),
|
||||
dict(name='div', attrs={'class':'comments_box'})
|
||||
]
|
||||
|
||||
preprocess_regexps = [(re.compile(r'\| <a href="#comments">Komentarze \([0-9]*\)</a>'), lambda match: '')]
|
||||
|
||||
remove_attributes = [ 'width', 'height' ]
|
||||
|
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
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/jezebel.com/img/logo.png'
|
||||
extra_css = '''
|
||||
@ -32,13 +32,12 @@ class Jezebel(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/vip?format=xml')]
|
||||
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/jezebel/full')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
@ -16,7 +16,7 @@ class Kotaku(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/kotaku.com/img/logo.png'
|
||||
extra_css = '''
|
||||
@ -31,13 +31,12 @@ class Kotaku(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/vip?format=xml')]
|
||||
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/kotaku/full')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
37
recipes/lupa.recipe
Normal file
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):
|
||||
title = 'Novinky'
|
||||
__author__ = 'Tomas Latal'
|
||||
__version__ = '1.0'
|
||||
__date__ = '24 April 2011'
|
||||
__version__ = '1.1'
|
||||
__date__ = '30 April 2011'
|
||||
description = 'News from server Novinky.cz'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 10
|
||||
@ -18,6 +18,7 @@ class NovinkyCZ(BasicNewsRecipe):
|
||||
publication_type = 'newsportal'
|
||||
no_stylesheets = True
|
||||
remove_javascript = True
|
||||
cover_url = 'http://img193.imageshack.us/img193/3039/novinkycover.jpg'
|
||||
extra_css = 'p.acmDescription{font-style:italic;} p.acmAuthor{font-size:0.8em; color:#707070}'
|
||||
|
||||
feeds = [
|
||||
|
37
recipes/podnikatel.recipe
Normal file
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
|
||||
no_stylesheets = True
|
||||
encoding = "utf-8"
|
||||
language = 'de_AT'
|
||||
language = 'de'
|
||||
|
||||
use_embedded_content =False
|
||||
remove_empty_feeds = True
|
||||
|
@ -10,6 +10,8 @@ import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Time(BasicNewsRecipe):
|
||||
recipe_disabled = ('This recipe has been disabled as TIME no longer'
|
||||
' publish complete articles on the web.')
|
||||
title = u'Time'
|
||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
||||
description = 'Weekly magazine'
|
||||
|
@ -7,13 +7,11 @@ usatoday.com
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag
|
||||
import re
|
||||
|
||||
class USAToday(BasicNewsRecipe):
|
||||
|
||||
title = 'USA Today'
|
||||
__author__ = 'GRiker'
|
||||
__author__ = 'Kovid Goyal'
|
||||
oldest_article = 1
|
||||
timefmt = ''
|
||||
max_articles_per_feed = 20
|
||||
@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe):
|
||||
margin-bottom: 0em; \
|
||||
font-size: smaller;}\n \
|
||||
.articleBody {text-align: left;}\n '
|
||||
conversion_options = { 'linearize_tables' : True }
|
||||
#simultaneous_downloads = 1
|
||||
feeds = [
|
||||
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
|
||||
@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe):
|
||||
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
|
||||
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':[
|
||||
'byLine',
|
||||
'inside-copy',
|
||||
'inside-head',
|
||||
'inside-head2',
|
||||
'item',
|
||||
'item-block',
|
||||
'photo-container',
|
||||
]}),
|
||||
dict(id=[
|
||||
'applyMainStoryPhoto',
|
||||
'permalink',
|
||||
])]
|
||||
keep_only_tags = [dict(attrs={'class':'story'})]
|
||||
remove_tags = [
|
||||
dict(attrs={'class':[
|
||||
'share',
|
||||
'reprints',
|
||||
'inline-h3',
|
||||
'info-extras',
|
||||
'ppy-outer',
|
||||
'ppy-caption',
|
||||
'comments',
|
||||
'jump',
|
||||
'pagetools',
|
||||
'post-attributes',
|
||||
'tags',
|
||||
'bottom-tools',
|
||||
'sponsoredlinks',
|
||||
]}),
|
||||
dict(id=['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):
|
||||
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
|
||||
@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe):
|
||||
masthead = None
|
||||
return masthead
|
||||
|
||||
def massageNCXText(self, description):
|
||||
# Kindle TOC descriptions won't render certain characters
|
||||
if description:
|
||||
massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
|
||||
# Replace '&' with '&'
|
||||
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,
|
||||
stdout_redirect, stderr_redirect);
|
||||
|
||||
if (stdout != NULL) fclose(stdout);
|
||||
if (stderr != NULL) fclose(stderr);
|
||||
|
||||
DeleteFile(stdout_redirect);
|
||||
DeleteFile(stderr_redirect);
|
||||
|
||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = u'calibre'
|
||||
numeric_version = (0, 7, 58)
|
||||
numeric_version = (0, 8, 0)
|
||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
|
@ -607,6 +607,7 @@ class StoreBase(Plugin): # {{{
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
minimum_calibre_version = (0, 8, 0)
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
|
@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
|
||||
from calibre.constants import numeric_version
|
||||
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
|
||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
from calibre.utils.config import test_eight_code
|
||||
|
||||
# To archive plugins {{{
|
||||
class HTML2ZIP(FileTypePlugin):
|
||||
@ -596,6 +595,7 @@ from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
|
||||
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
|
||||
from calibre.devices.nook.driver import NOOK, NOOK_COLOR
|
||||
from calibre.devices.prs505.driver import PRS505
|
||||
from calibre.devices.user_defined.driver import USER_DEFINED
|
||||
from calibre.devices.android.driver import ANDROID, S60
|
||||
from calibre.devices.nokia.driver import N770, N810, E71X, E52
|
||||
from calibre.devices.eslick.driver import ESLICK, EBK52
|
||||
@ -613,6 +613,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \
|
||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||
from calibre.devices.kobo.driver import KOBO
|
||||
from calibre.devices.bambook.driver import BAMBOOK
|
||||
from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX
|
||||
|
||||
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
|
||||
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
|
||||
@ -621,28 +622,16 @@ from calibre.ebooks.epub.fix.epubcheck import Epubcheck
|
||||
plugins = [HTML2ZIP, PML2PMLZ, TXT2TXTZ, ArchiveExtract, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
||||
Epubcheck, ]
|
||||
|
||||
if test_eight_code:
|
||||
# New metadata download plugins {{{
|
||||
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
||||
from calibre.ebooks.metadata.sources.amazon import Amazon
|
||||
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
||||
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
|
||||
from calibre.ebooks.metadata.sources.overdrive import OverDrive
|
||||
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
||||
from calibre.ebooks.metadata.sources.amazon import Amazon
|
||||
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
||||
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
|
||||
from calibre.ebooks.metadata.sources.overdrive import OverDrive
|
||||
|
||||
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive]
|
||||
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive]
|
||||
|
||||
# }}}
|
||||
else:
|
||||
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
|
||||
KentDistrictLibrary
|
||||
from calibre.ebooks.metadata.douban import DoubanBooks
|
||||
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
|
||||
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
|
||||
AmazonCovers, DoubanCovers
|
||||
|
||||
plugins += [GoogleBooks, ISBNDB, Amazon,
|
||||
OpenLibraryCovers, AmazonCovers, DoubanCovers,
|
||||
NiceBooksCovers, KentDistrictLibrary, DoubanBooks, NiceBooks]
|
||||
|
||||
plugins += [
|
||||
ComicInput,
|
||||
@ -755,6 +744,9 @@ plugins += [
|
||||
EEEREADER,
|
||||
NEXTBOOK,
|
||||
ITUNES,
|
||||
BOEYE_BEX,
|
||||
BOEYE_BDX,
|
||||
USER_DEFINED,
|
||||
]
|
||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||
x.__name__.endswith('MetadataReader')]
|
||||
@ -867,10 +859,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
||||
|
||||
if test_eight_code:
|
||||
plugins += [ActionStore]
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
|
||||
|
||||
# }}}
|
||||
|
||||
@ -1096,10 +1085,8 @@ class Misc(PreferencesPlugin):
|
||||
|
||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
|
||||
|
||||
if test_eight_code:
|
||||
plugins.append(MetadataSources)
|
||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
|
||||
MetadataSources]
|
||||
|
||||
#}}}
|
||||
|
||||
|
@ -15,12 +15,11 @@ from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
from calibre.customize.builtins import plugins as builtin_plugins
|
||||
from calibre.devices.interface import DevicePlugin
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.metadata.covers import CoverDownload
|
||||
from calibre.ebooks.metadata.fetch import MetadataSource
|
||||
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
||||
plugin_dir, OptionParser, prefs
|
||||
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
|
||||
plugin_dir, OptionParser)
|
||||
from calibre.ebooks.epub.fix import ePubFixer
|
||||
from calibre.ebooks.metadata.sources.base import Source
|
||||
from calibre.constants import DEBUG
|
||||
|
||||
builtin_names = frozenset([p.name for p in builtin_plugins])
|
||||
|
||||
@ -93,8 +92,7 @@ def restore_plugin_state_to_default(plugin_or_name):
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
default_disabled_plugins = set([
|
||||
'Douban Books', 'Douban.com covers', 'Nicebooks', 'Nicebooks covers',
|
||||
'Kent District Library'
|
||||
'Overdrive',
|
||||
])
|
||||
|
||||
def is_disabled(plugin):
|
||||
@ -190,44 +188,6 @@ def output_profiles():
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Metadata sources {{{
|
||||
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, MetadataSource) and \
|
||||
plugin.metadata_type == metadata_type:
|
||||
if is_disabled(plugin):
|
||||
continue
|
||||
if customize:
|
||||
customization = config['plugin_customization']
|
||||
plugin.site_customization = customization.get(plugin.name, None)
|
||||
if plugin.name == 'IsbnDB' and isbndb_key is not None:
|
||||
plugin.site_customization = isbndb_key
|
||||
yield plugin
|
||||
|
||||
def get_isbndb_key():
|
||||
return config['plugin_customization'].get('IsbnDB', None)
|
||||
|
||||
def set_isbndb_key(key):
|
||||
for plugin in _initialized_plugins:
|
||||
if plugin.name == 'IsbnDB':
|
||||
return customize_plugin(plugin, key)
|
||||
|
||||
def migrate_isbndb_key():
|
||||
key = prefs['isbndb_com_key']
|
||||
if key:
|
||||
prefs.set('isbndb_com_key', '')
|
||||
set_isbndb_key(key)
|
||||
|
||||
def cover_sources():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, CoverDownload):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
|
||||
# }}}
|
||||
|
||||
# Interface Actions # {{{
|
||||
|
||||
def interface_actions():
|
||||
@ -527,8 +487,9 @@ def initialize_plugins():
|
||||
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
|
||||
_initialized_plugins.append(plugin)
|
||||
except:
|
||||
print 'Failed to initialize plugin...'
|
||||
traceback.print_exc()
|
||||
print 'Failed to initialize plugin:', repr(zfp)
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
|
||||
reread_filetype_plugins()
|
||||
reread_metadata_plugins()
|
||||
|
@ -156,3 +156,60 @@ def debug(ioreg_to_tmp=False, buf=None):
|
||||
sys.stdout = oldo
|
||||
sys.stderr = olde
|
||||
|
||||
def device_info(ioreg_to_tmp=False, buf=None):
|
||||
from calibre.devices.scanner import DeviceScanner, win_pnp_drives
|
||||
from calibre.constants import iswindows
|
||||
import re
|
||||
|
||||
res = {}
|
||||
device_details = {}
|
||||
device_set = set()
|
||||
drive_details = {}
|
||||
drive_set = set()
|
||||
res['device_set'] = device_set
|
||||
res['device_details'] = device_details
|
||||
res['drive_details'] = drive_details
|
||||
res['drive_set'] = drive_set
|
||||
|
||||
try:
|
||||
s = DeviceScanner()
|
||||
s.scan()
|
||||
devices = (s.devices)
|
||||
if not iswindows:
|
||||
devices = [list(x) for x in devices]
|
||||
for dev in devices:
|
||||
for i in range(3):
|
||||
dev[i] = hex(dev[i])
|
||||
d = dev[0] + dev[1] + dev[2]
|
||||
device_set.add(d)
|
||||
device_details[d] = dev[0:3]
|
||||
else:
|
||||
for dev in devices:
|
||||
vid = re.search('vid_([0-9a-f]*)&', dev)
|
||||
if vid:
|
||||
vid = vid.group(1)
|
||||
pid = re.search('pid_([0-9a-f]*)&', dev)
|
||||
if pid:
|
||||
pid = pid.group(1)
|
||||
rev = re.search('rev_([0-9a-f]*)$', dev)
|
||||
if rev:
|
||||
rev = rev.group(1)
|
||||
d = vid+pid+rev
|
||||
device_set.add(d)
|
||||
device_details[d] = (vid, pid, rev)
|
||||
|
||||
drives = win_pnp_drives(debug=False)
|
||||
for drive,details in drives.iteritems():
|
||||
order = 'ORD_' + str(drive.order)
|
||||
ven = re.search('VEN_([^&]*)&', details)
|
||||
if ven:
|
||||
ven = ven.group(1)
|
||||
prod = re.search('PROD_([^&]*)&', details)
|
||||
if prod:
|
||||
prod = prod.group(1)
|
||||
d = (order, ven, prod)
|
||||
drive_details[drive] = d
|
||||
drive_set.add(drive)
|
||||
finally:
|
||||
pass
|
||||
return res
|
||||
|
@ -62,7 +62,7 @@ class ANDROID(USBMS):
|
||||
0x502 : { 0x3203 : [0x0100]},
|
||||
|
||||
# Dell
|
||||
0x413c : { 0xb007 : [0x0100, 0x0224]},
|
||||
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
|
||||
|
||||
# LG
|
||||
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
|
||||
@ -112,7 +112,7 @@ class ANDROID(USBMS):
|
||||
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB']
|
||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD']
|
||||
|
||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||
|
||||
|
@ -163,6 +163,8 @@ class ITUNES(DriverBase):
|
||||
settings()
|
||||
set_progress_reporter()
|
||||
upload_books()
|
||||
_get_fpath()
|
||||
_update_epub_metadata()
|
||||
add_books_to_metadata()
|
||||
use_plugboard_ext()
|
||||
set_plugboard()
|
||||
@ -504,7 +506,7 @@ class ITUNES(DriverBase):
|
||||
if self.iTunes:
|
||||
# Check for connected book-capable device
|
||||
self.sources = self._get_sources()
|
||||
if 'iPod' in self.sources:
|
||||
if 'iPod' in self.sources and not self.ejected:
|
||||
#if DEBUG:
|
||||
#sys.stdout.write('.')
|
||||
#sys.stdout.flush()
|
||||
@ -2034,16 +2036,17 @@ class ITUNES(DriverBase):
|
||||
if 'iPod' in self.sources:
|
||||
connected_device = self.sources['iPod']
|
||||
device = self.iTunes.sources[connected_device]
|
||||
dev_books = None
|
||||
for pl in device.playlists():
|
||||
if pl.special_kind() == appscript.k.Books:
|
||||
if DEBUG:
|
||||
self.log.info(" Book playlist: '%s'" % (pl.name()))
|
||||
books = pl.file_tracks()
|
||||
dev_books = pl.file_tracks()
|
||||
break
|
||||
else:
|
||||
self.log.error(" book_playlist not found")
|
||||
|
||||
for book in books:
|
||||
for book in dev_books:
|
||||
# This may need additional entries for international iTunes users
|
||||
if book.kind() in self.Audiobooks:
|
||||
if DEBUG:
|
||||
@ -2621,45 +2624,42 @@ class ITUNES(DriverBase):
|
||||
# Touch the OPF timestamp
|
||||
try:
|
||||
zf_opf = ZipFile(fpath,'r')
|
||||
fnames = zf_opf.namelist()
|
||||
opf = [x for x in fnames if '.opf' in x][0]
|
||||
except:
|
||||
raise UserFeedback("'%s' is not a valid EPUB" % metadata.title,
|
||||
None,
|
||||
level=UserFeedback.WARN)
|
||||
fnames = zf_opf.namelist()
|
||||
try:
|
||||
opf = [x for x in fnames if '.opf' in x][0]
|
||||
except:
|
||||
opf = None
|
||||
if opf:
|
||||
opf_tree = etree.fromstring(zf_opf.read(opf))
|
||||
md_els = opf_tree.xpath('.//*[local-name()="metadata"]')
|
||||
if md_els:
|
||||
ts = md_els[0].find('.//*[@name="calibre:timestamp"]')
|
||||
if ts is not None:
|
||||
timestamp = ts.get('content')
|
||||
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)
|
||||
|
||||
opf_tree = etree.fromstring(zf_opf.read(opf))
|
||||
md_els = opf_tree.xpath('.//*[local-name()="metadata"]')
|
||||
if md_els:
|
||||
ts = md_els[0].find('.//*[@name="calibre:timestamp"]')
|
||||
if ts is not None:
|
||||
timestamp = ts.get('content')
|
||||
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.warning(" missing <metadata> block in OPF file")
|
||||
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
|
||||
|
||||
else:
|
||||
metadata.timestamp = now()
|
||||
if DEBUG:
|
||||
if metadata.language != metadata_x.language:
|
||||
self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
|
||||
self.log.warning(" missing <metadata> block in OPF file")
|
||||
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()
|
||||
|
||||
|
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
|
||||
|
||||
def linux_swap_drives(self, drives):
|
||||
if len(drives) < 2: return drives
|
||||
if len(drives) < 2 or not drives[1] or not drives[2]: return drives
|
||||
drives = list(drives)
|
||||
t = drives[0]
|
||||
drives[0] = drives[1]
|
||||
@ -95,7 +95,6 @@ class HANLINV5(HANLINV3):
|
||||
gui_name = 'Hanlin V5'
|
||||
description = _('Communicate with Hanlin V5 eBook readers.')
|
||||
|
||||
|
||||
VENDOR_ID = [0x0492]
|
||||
PRODUCT_ID = [0x8813]
|
||||
BCD = [0x319]
|
||||
|
@ -164,7 +164,7 @@ class APNXBuilder(object):
|
||||
if c == '/':
|
||||
closing = True
|
||||
continue
|
||||
elif c in ('d', 'p'):
|
||||
elif c == 'p':
|
||||
if closing:
|
||||
in_p = False
|
||||
else:
|
||||
|
@ -187,7 +187,7 @@ class LUMIREAD(USBMS):
|
||||
cfilepath = cfilepath.replace(os.sep+'books'+os.sep,
|
||||
os.sep+'covers'+os.sep, 1)
|
||||
pdir = os.path.dirname(cfilepath)
|
||||
if not os.exists(pdir):
|
||||
if not os.path.exists(pdir):
|
||||
os.makedirs(pdir)
|
||||
with open(cfilepath+'.jpg', 'wb') as f:
|
||||
f.write(metadata.thumbnail[-1])
|
||||
|
@ -94,6 +94,9 @@ class DeviceConfig(object):
|
||||
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
|
||||
ec = []
|
||||
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
|
||||
if config_widget.opt_extra_customization[i] is None:
|
||||
ec.append(None)
|
||||
continue
|
||||
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
|
||||
ec.append(config_widget.opt_extra_customization[i].isChecked())
|
||||
else:
|
||||
|
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'
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
|
||||
from calibre import walk
|
||||
from calibre import guess_type, walk
|
||||
from calibre.customize.conversion import InputFormatPlugin
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
|
||||
class HTMLZInput(InputFormatPlugin):
|
||||
@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin):
|
||||
|
||||
# Extract content from zip archive.
|
||||
zf = ZipFile(stream)
|
||||
zf.extractall('.')
|
||||
zf.extractall()
|
||||
|
||||
for x in walk('.'):
|
||||
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
|
||||
@ -70,5 +72,24 @@ class HTMLZInput(InputFormatPlugin):
|
||||
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
|
||||
mi = get_file_type_metadata(stream, file_ext)
|
||||
meta_info_to_oeb_metadata(mi, oeb.metadata, log)
|
||||
|
||||
# Get the cover path from the OPF.
|
||||
cover_href = None
|
||||
opf = None
|
||||
for x in walk('.'):
|
||||
if os.path.splitext(x)[1].lower() in ('.opf'):
|
||||
opf = x
|
||||
break
|
||||
if opf:
|
||||
opf = OPF(opf)
|
||||
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
|
||||
# Set the cover.
|
||||
if cover_href:
|
||||
cdata = None
|
||||
with open(cover_href, 'rb') as cf:
|
||||
cdata = cf.read()
|
||||
id, href = oeb.manifest.generate('cover', cover_href)
|
||||
oeb.manifest.add(id, href, guess_type(cover_href)[0], data=cdata)
|
||||
oeb.guide.add('cover', 'Cover', href)
|
||||
|
||||
return oeb
|
||||
|
@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from cStringIO import StringIO
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre.customize.conversion import OutputFormatPlugin, \
|
||||
OptionRecommendation
|
||||
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
|
||||
@ -79,10 +81,31 @@ class HTMLZOutput(OutputFormatPlugin):
|
||||
fname = os.path.join(tdir, 'images', images[item.href])
|
||||
with open(fname, 'wb') as img:
|
||||
img.write(data)
|
||||
|
||||
# Cover
|
||||
cover_path = None
|
||||
try:
|
||||
cover_data = None
|
||||
if oeb_book.metadata.cover:
|
||||
term = oeb_book.metadata.cover[0].term
|
||||
cover_data = oeb_book.guide[term].item.data
|
||||
if cover_data:
|
||||
from calibre.utils.magick.draw import save_cover_data_to
|
||||
cover_path = os.path.join(tdir, 'cover.jpg')
|
||||
with open(cover_path, 'w') as cf:
|
||||
cf.write('')
|
||||
save_cover_data_to(cover_data, cover_path)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Metadata
|
||||
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
|
||||
mdataf.write(etree.tostring(oeb_book.metadata.to_opf1()))
|
||||
opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1())))
|
||||
mi = opf.to_book_metadata()
|
||||
if cover_path:
|
||||
mi.cover = 'cover.jpg'
|
||||
mdataf.write(metadata_to_opf(mi))
|
||||
|
||||
htmlz = ZipFile(output_path, 'w')
|
||||
htmlz.add_dir(tdir)
|
||||
|
@ -274,6 +274,9 @@ def check_isbn(isbn):
|
||||
if not isbn:
|
||||
return None
|
||||
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
|
||||
all_same = re.match(r'(\d)\1{9,12}$', isbn)
|
||||
if all_same is not None:
|
||||
return None
|
||||
if len(isbn) == 10:
|
||||
return check_isbn10(isbn)
|
||||
if len(isbn) == 13:
|
||||
|
@ -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 calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.zipfile import ZipFile, safe_replace
|
||||
|
||||
@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True):
|
||||
opf = OPF(opf_stream)
|
||||
mi = opf.to_book_metadata()
|
||||
if extract_cover:
|
||||
cover_name = opf.raster_cover
|
||||
if cover_name:
|
||||
mi.cover_data = ('jpg', zf.read(cover_name))
|
||||
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
|
||||
if cover_href:
|
||||
mi.cover_data = ('jpg', zf.read(cover_href))
|
||||
except:
|
||||
return mi
|
||||
return mi
|
||||
@ -59,17 +59,20 @@ def set_metadata(stream, mi):
|
||||
except:
|
||||
pass
|
||||
if new_cdata:
|
||||
raster_cover = opf.raster_cover
|
||||
if not raster_cover:
|
||||
raster_cover = 'cover.jpg'
|
||||
cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover)
|
||||
cover = opf.cover
|
||||
if not cover:
|
||||
cover = 'cover.jpg'
|
||||
cpath = posixpath.join(posixpath.dirname(opf_path), cover)
|
||||
new_cover = _write_new_cover(new_cdata, cpath)
|
||||
replacements[cpath] = open(new_cover.name, 'rb')
|
||||
mi.cover = cover
|
||||
|
||||
# Update the metadata.
|
||||
opf.smart_update(mi, replace_metadata=True)
|
||||
old_mi = opf.to_book_metadata()
|
||||
old_mi.smart_update(mi)
|
||||
opf.smart_update(metadata_to_opf(old_mi), replace_metadata=True)
|
||||
newopf = StringIO(opf.render())
|
||||
safe_replace(stream, opf_path, newopf, extra_replacements=replacements)
|
||||
safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True)
|
||||
|
||||
# Cleanup temporary files.
|
||||
try:
|
||||
|
@ -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')
|
||||
for item in self.itermanifest():
|
||||
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
|
||||
def cover(self):
|
||||
|
@ -307,7 +307,7 @@ class Source(Plugin):
|
||||
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||
[
|
||||
# Remove things like: (2010) (Omnibus) etc.
|
||||
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''),
|
||||
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''),
|
||||
# Remove any strings that contain the substring edition inside
|
||||
# parentheses
|
||||
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
|
||||
|
@ -19,13 +19,8 @@ from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
from calibre.ebooks.metadata.sources.base import create_log
|
||||
from calibre.ebooks.metadata.sources.identify import identify
|
||||
from calibre.ebooks.metadata.sources.covers import download_cover
|
||||
from calibre.utils.config import test_eight_code
|
||||
|
||||
def option_parser():
|
||||
if not test_eight_code:
|
||||
from calibre.ebooks.metadata.fetch import option_parser
|
||||
return option_parser()
|
||||
|
||||
parser = OptionParser(textwrap.dedent(
|
||||
'''\
|
||||
%prog [options]
|
||||
@ -48,9 +43,6 @@ def option_parser():
|
||||
return parser
|
||||
|
||||
def main(args=sys.argv):
|
||||
if not test_eight_code:
|
||||
from calibre.ebooks.metadata.fetch import main
|
||||
return main(args)
|
||||
parser = option_parser()
|
||||
opts, args = parser.parse_args(args)
|
||||
|
||||
|
@ -13,6 +13,7 @@ from Queue import Queue, Empty
|
||||
from threading import Thread
|
||||
from io import BytesIO
|
||||
from operator import attrgetter
|
||||
from urlparse import urlparse
|
||||
|
||||
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
|
||||
from calibre.ebooks.metadata.sources.base import create_log, msprefs
|
||||
@ -400,6 +401,9 @@ def identify(log, abort, # {{{
|
||||
and plugin.get_cached_cover_url(result.identifiers) is not
|
||||
None)
|
||||
result.identify_plugin = plugin
|
||||
if msprefs['txt_comments']:
|
||||
if plugin.has_html_comments and result.comments:
|
||||
result.comments = html2text(r.comments)
|
||||
|
||||
log('The identify phase took %.2f seconds'%(time.time() - start_time))
|
||||
log('The longest time (%f) was taken by:'%longest, lp)
|
||||
@ -410,10 +414,6 @@ def identify(log, abort, # {{{
|
||||
log('We have %d merged results, merging took: %.2f seconds' %
|
||||
(len(results), time.time() - start_time))
|
||||
|
||||
if msprefs['txt_comments']:
|
||||
for r in results:
|
||||
if r.identify_plugin.has_html_comments and r.comments:
|
||||
r.comments = html2text(r.comments)
|
||||
|
||||
max_tags = msprefs['max_tags']
|
||||
for r in results:
|
||||
@ -459,6 +459,14 @@ def urls_from_identifiers(identifiers): # {{{
|
||||
if oclc:
|
||||
ans.append(('OCLC', 'oclc', oclc,
|
||||
'http://www.worldcat.org/oclc/'+oclc))
|
||||
url = identifiers.get('uri', None)
|
||||
if url is None:
|
||||
url = identifiers.get('url', None)
|
||||
if url and url.startswith('http'):
|
||||
url = url[:8].replace('|', ':') + url[8:].replace('|', ',')
|
||||
parts = urlparse(url)
|
||||
name = parts.netloc
|
||||
ans.append((name, 'url', url, url))
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
|
@ -41,7 +41,7 @@ class OverDrive(Source):
|
||||
cached_cover_url_is_reliable = True
|
||||
|
||||
options = (
|
||||
Option('get_full_metadata', 'bool', False,
|
||||
Option('get_full_metadata', 'bool', True,
|
||||
_('Download all metadata (slow)'),
|
||||
_('Enable this option to gather all metadata available from Overdrive.')),
|
||||
)
|
||||
|
@ -253,6 +253,8 @@ class MobiReader(object):
|
||||
|
||||
.italic { font-style: italic }
|
||||
|
||||
.underline { text-decoration: underline }
|
||||
|
||||
.mbp_pagebreak {
|
||||
page-break-after: always; margin: 0; display: block
|
||||
}
|
||||
@ -601,6 +603,9 @@ class MobiReader(object):
|
||||
elif tag.tag == 'i':
|
||||
tag.tag = 'span'
|
||||
tag.attrib['class'] = 'italic'
|
||||
elif tag.tag == 'u':
|
||||
tag.tag = 'span'
|
||||
tag.attrib['class'] = 'underline'
|
||||
elif tag.tag == 'b':
|
||||
tag.tag = 'span'
|
||||
tag.attrib['class'] = 'bold'
|
||||
|
@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en'
|
||||
Convert an ODT file into a Open Ebook
|
||||
'''
|
||||
import os
|
||||
|
||||
from lxml import etree
|
||||
from odf.odf2xhtml import ODF2XHTML
|
||||
|
||||
from calibre import CurrentDir, walk
|
||||
@ -23,7 +25,51 @@ class Extract(ODF2XHTML):
|
||||
with open(name, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
def __call__(self, stream, odir):
|
||||
def filter_css(self, html, log):
|
||||
root = etree.fromstring(html)
|
||||
style = root.xpath('//*[local-name() = "style" and @type="text/css"]')
|
||||
if style:
|
||||
style = style[0]
|
||||
css = style.text
|
||||
if css:
|
||||
style.text, sel_map = self.do_filter_css(css)
|
||||
for x in root.xpath('//*[@class]'):
|
||||
extra = []
|
||||
orig = x.get('class')
|
||||
for cls in orig.split():
|
||||
extra.extend(sel_map.get(cls, []))
|
||||
if extra:
|
||||
x.set('class', orig + ' ' + ' '.join(extra))
|
||||
html = etree.tostring(root, encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
return html
|
||||
|
||||
def do_filter_css(self, css):
|
||||
from cssutils import parseString
|
||||
from cssutils.css import CSSRule
|
||||
sheet = parseString(css)
|
||||
rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
|
||||
sel_map = {}
|
||||
count = 0
|
||||
for r in rules:
|
||||
# Check if we have only class selectors for this rule
|
||||
nc = [x for x in r.selectorList if not
|
||||
x.selectorText.startswith('.')]
|
||||
if len(r.selectorList) > 1 and not nc:
|
||||
# Replace all the class selectors with a single class selector
|
||||
# This will be added to the class attribute of all elements
|
||||
# that have one of these selectors.
|
||||
replace_name = 'c_odt%d'%count
|
||||
count += 1
|
||||
for sel in r.selectorList:
|
||||
s = sel.selectorText[1:]
|
||||
if s not in sel_map:
|
||||
sel_map[s] = []
|
||||
sel_map[s].append(replace_name)
|
||||
r.selectorText = '.'+replace_name
|
||||
return sheet.cssText, sel_map
|
||||
|
||||
def __call__(self, stream, odir, log):
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||
@ -32,13 +78,17 @@ class Extract(ODF2XHTML):
|
||||
if not os.path.exists(odir):
|
||||
os.makedirs(odir)
|
||||
with CurrentDir(odir):
|
||||
print 'Extracting ODT file...'
|
||||
log('Extracting ODT file...')
|
||||
html = self.odf2xhtml(stream)
|
||||
# A blanket img specification like this causes problems
|
||||
# with EPUB output as the contaiing element often has
|
||||
# with EPUB output as the containing element often has
|
||||
# an absolute height and width set that is larger than
|
||||
# the available screen real estate
|
||||
html = html.replace('img { width: 100%; height: 100%; }', '')
|
||||
try:
|
||||
html = self.filter_css(html, log)
|
||||
except:
|
||||
log.exception('Failed to filter CSS, conversion may be slow')
|
||||
with open('index.xhtml', 'wb') as f:
|
||||
f.write(html.encode('utf-8'))
|
||||
zf = ZipFile(stream, 'r')
|
||||
@ -67,7 +117,7 @@ class ODTInput(InputFormatPlugin):
|
||||
|
||||
def convert(self, stream, options, file_ext, log,
|
||||
accelerators):
|
||||
return Extract()(stream, '.')
|
||||
return Extract()(stream, '.', log)
|
||||
|
||||
def postprocess_book(self, oeb, opts, log):
|
||||
# Fix <p><div> constructs as the asinine epubchecker complains
|
||||
|
@ -1049,8 +1049,8 @@ class Manifest(object):
|
||||
|
||||
# Remove hyperlinks with no content as they cause rendering
|
||||
# artifacts in browser based renderers
|
||||
# Also remove empty <b> and <i> tags
|
||||
for a in xpath(data, '//h:a[@href]|//h:i|//h:b'):
|
||||
# Also remove empty <b>, <u> and <i> tags
|
||||
for a in xpath(data, '//h:a[@href]|//h:i|//h:b|//h:u'):
|
||||
if a.get('id', None) is None and a.get('name', None) is None \
|
||||
and len(a) == 0 and not a.text:
|
||||
remove_elem(a)
|
||||
|
@ -124,15 +124,20 @@ class Stylizer(object):
|
||||
|
||||
def __init__(self, tree, path, oeb, opts, profile=None,
|
||||
extra_css='', user_css=''):
|
||||
from calibre.customize.ui import input_profiles
|
||||
self.oeb, self.opts = oeb, opts
|
||||
self.profile = None
|
||||
for x in input_profiles():
|
||||
if x.short_name == 'sony':
|
||||
self.profile = x
|
||||
break
|
||||
self.profile = profile
|
||||
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
|
||||
item = oeb.manifest.hrefs[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.add('description', mi.comments)
|
||||
elif override_input_metadata:
|
||||
m.clear('description')
|
||||
m.clear('description')
|
||||
if not mi.is_null('publisher'):
|
||||
m.clear('publisher')
|
||||
m.add('publisher', mi.publisher)
|
||||
|
@ -32,10 +32,11 @@ class PDFInput(InputFormatPlugin):
|
||||
|
||||
def convert_new(self, stream, accelerators):
|
||||
from calibre.ebooks.pdf.reflow import PDFDocument
|
||||
from calibre.utils.cleantext import clean_ascii_chars
|
||||
if pdfreflow_err:
|
||||
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
|
||||
pdfreflow.reflow(stream.read(), 1, -1)
|
||||
xml = open('index.xml', 'rb').read()
|
||||
xml = clean_ascii_chars(open('index.xml', 'rb').read())
|
||||
PDFDocument(xml, self.opts, self.log)
|
||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||
|
||||
|
@ -15,7 +15,6 @@ import cStringIO
|
||||
from lxml import etree
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.filenames import ascii_text
|
||||
from calibre.utils.magick.draw import save_cover_data_to, identify_data
|
||||
|
||||
TAGS = {
|
||||
@ -79,8 +78,7 @@ def txt2rtf(text):
|
||||
elif val <= 127:
|
||||
buf.write(x)
|
||||
else:
|
||||
repl = ascii_text(x)
|
||||
c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl))
|
||||
c = r'\u{0:d}?'.format(val)
|
||||
buf.write(c)
|
||||
return buf.getvalue()
|
||||
|
||||
|
@ -34,7 +34,7 @@ if isosx:
|
||||
)
|
||||
gprefs.defaults['action-layout-toolbar'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
|
||||
'Connect Share', None, 'Remove Books',
|
||||
)
|
||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||
@ -48,7 +48,7 @@ else:
|
||||
gprefs.defaults['action-layout-menubar-device'] = ()
|
||||
gprefs.defaults['action-layout-toolbar'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
|
||||
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
|
||||
)
|
||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||
|
@ -20,9 +20,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2 import config, question_dialog
|
||||
from calibre.gui2 import question_dialog
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.utils.config import test_eight_code
|
||||
from calibre.ebooks.metadata.sources.base import msprefs
|
||||
|
||||
def get_filters():
|
||||
@ -180,26 +179,17 @@ class AddAction(InterfaceAction):
|
||||
except IndexError:
|
||||
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
|
||||
self.isbn_add_dialog.accept()
|
||||
if test_eight_code:
|
||||
orig = msprefs['ignore_fields']
|
||||
new = list(orig)
|
||||
for x in ('title', 'authors'):
|
||||
if x in new:
|
||||
new.remove(x)
|
||||
msprefs['ignore_fields'] = new
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].download_metadata(
|
||||
ids=self.add_by_isbn_ids)
|
||||
finally:
|
||||
msprefs['ignore_fields'] = orig
|
||||
else:
|
||||
orig = config['overwrite_author_title_metadata']
|
||||
config['overwrite_author_title_metadata'] = True
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].do_download_metadata(
|
||||
self.add_by_isbn_ids)
|
||||
finally:
|
||||
config['overwrite_author_title_metadata'] = orig
|
||||
orig = msprefs['ignore_fields']
|
||||
new = list(orig)
|
||||
for x in ('title', 'authors'):
|
||||
if x in new:
|
||||
new.remove(x)
|
||||
msprefs['ignore_fields'] = new
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].download_metadata(
|
||||
ids=self.add_by_isbn_ids)
|
||||
finally:
|
||||
msprefs['ignore_fields'] = orig
|
||||
return
|
||||
|
||||
|
||||
|
@ -246,7 +246,8 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
def delete_requested(self, name, location):
|
||||
loc = location.replace('/', os.sep)
|
||||
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
|
||||
_('All files from %s will be '
|
||||
_('<b style="color: red">All files</b> (not just ebooks) '
|
||||
'from <br><br><b>%s</b><br><br> will be '
|
||||
'<b>permanently deleted</b>. Are you sure?') % loc,
|
||||
show_copy_button=False):
|
||||
return
|
||||
|
@ -10,15 +10,13 @@ from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
|
||||
|
||||
from calibre.gui2 import error_dialog, config, Dispatcher, question_dialog
|
||||
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
||||
from calibre.gui2 import error_dialog, Dispatcher, question_dialog
|
||||
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import test_eight_code
|
||||
|
||||
class EditMetadataAction(InterfaceAction):
|
||||
|
||||
@ -36,22 +34,8 @@ class EditMetadataAction(InterfaceAction):
|
||||
md.addAction(_('Edit metadata in bulk'),
|
||||
partial(self.edit_metadata, False, bulk=True))
|
||||
md.addSeparator()
|
||||
if test_eight_code:
|
||||
dall = self.download_metadata
|
||||
else:
|
||||
dall = partial(self.download_metadata_old, False, covers=True)
|
||||
dident = partial(self.download_metadata_old, False, covers=False)
|
||||
dcovers = partial(self.download_metadata_old, False, covers=True,
|
||||
set_metadata=False, set_social_metadata=False)
|
||||
|
||||
md.addAction(_('Download metadata and covers'), dall,
|
||||
md.addAction(_('Download metadata and covers'), self.download_metadata,
|
||||
Qt.ControlModifier+Qt.Key_D)
|
||||
if not test_eight_code:
|
||||
md.addAction(_('Download only metadata'), dident)
|
||||
md.addAction(_('Download only covers'), dcovers)
|
||||
md.addAction(_('Download only social metadata'),
|
||||
partial(self.download_metadata_old, False, covers=False,
|
||||
set_metadata=False, set_social_metadata=True))
|
||||
self.metadata_menu = md
|
||||
|
||||
mb = QMenu()
|
||||
@ -88,7 +72,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
_('No books selected'), show=True)
|
||||
db = self.gui.library_view.model().db
|
||||
ids = [db.id(row.row()) for row in rows]
|
||||
from calibre.gui2.metadata.bulk_download2 import start_download
|
||||
from calibre.gui2.metadata.bulk_download import start_download
|
||||
start_download(self.gui, ids,
|
||||
Dispatcher(self.metadata_downloaded))
|
||||
|
||||
@ -96,7 +80,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
if job.failed:
|
||||
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
|
||||
return
|
||||
from calibre.gui2.metadata.bulk_download2 import get_job_details
|
||||
from calibre.gui2.metadata.bulk_download import get_job_details
|
||||
id_map, failed_ids, failed_covers, all_failed, det_msg = \
|
||||
get_job_details(job)
|
||||
if all_failed:
|
||||
@ -112,8 +96,9 @@ class EditMetadataAction(InterfaceAction):
|
||||
show_copy_button = False
|
||||
if failed_ids or failed_covers:
|
||||
show_copy_button = True
|
||||
num = len(failed_ids.union(failed_covers))
|
||||
msg += '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
|
||||
' "Show details" to see which books.')%len(failed_ids)
|
||||
' "Show details" to see which books.')%num
|
||||
|
||||
payload = (id_map, failed_ids, failed_covers)
|
||||
from calibre.gui2.dialogs.message_box import ProceedNotification
|
||||
@ -158,49 +143,6 @@ class EditMetadataAction(InterfaceAction):
|
||||
|
||||
self.apply_metadata_changes(id_map)
|
||||
|
||||
def download_metadata_old(self, checked, covers=True, set_metadata=True,
|
||||
set_social_metadata=None):
|
||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
d = error_dialog(self.gui, _('Cannot download metadata'),
|
||||
_('No books selected'))
|
||||
d.exec_()
|
||||
return
|
||||
db = self.gui.library_view.model().db
|
||||
ids = [db.id(row.row()) for row in rows]
|
||||
self.do_download_metadata(ids, covers=covers,
|
||||
set_metadata=set_metadata,
|
||||
set_social_metadata=set_social_metadata)
|
||||
|
||||
def do_download_metadata(self, ids, covers=True, set_metadata=True,
|
||||
set_social_metadata=None):
|
||||
m = self.gui.library_view.model()
|
||||
db = m.db
|
||||
if set_social_metadata is None:
|
||||
get_social_metadata = config['get_social_metadata']
|
||||
else:
|
||||
get_social_metadata = set_social_metadata
|
||||
from calibre.gui2.metadata.bulk_download import DoDownload
|
||||
if set_social_metadata is not None and set_social_metadata:
|
||||
x = _('social metadata')
|
||||
else:
|
||||
x = _('covers') if covers and not set_metadata else _('metadata')
|
||||
title = _('Downloading {0} for {1} book(s)').format(x, len(ids))
|
||||
self._download_book_metadata = DoDownload(self.gui, title, db, ids,
|
||||
get_covers=covers, set_metadata=set_metadata,
|
||||
get_social_metadata=get_social_metadata)
|
||||
m.stop_metadata_backup()
|
||||
try:
|
||||
self._download_book_metadata.exec_()
|
||||
finally:
|
||||
m.start_metadata_backup()
|
||||
cr = self.gui.library_view.currentIndex().row()
|
||||
x = self._download_book_metadata
|
||||
if x.updated:
|
||||
self.gui.library_view.model().refresh_ids(
|
||||
x.updated, cr)
|
||||
if self.gui.cover_flow:
|
||||
self.gui.cover_flow.dataChanged()
|
||||
# }}}
|
||||
|
||||
def edit_metadata(self, checked, bulk=None):
|
||||
@ -227,9 +169,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
|
||||
current_row = row_list.index(cr)
|
||||
|
||||
func = (self.do_edit_metadata if test_eight_code else
|
||||
self.do_edit_metadata_old)
|
||||
changed, rows_to_refresh = func(row_list, current_row)
|
||||
changed, rows_to_refresh = self.do_edit_metadata(row_list, current_row)
|
||||
|
||||
m = self.gui.library_view.model()
|
||||
|
||||
@ -244,36 +184,6 @@ class EditMetadataAction(InterfaceAction):
|
||||
m.current_changed(current, previous)
|
||||
self.gui.tags_view.recount()
|
||||
|
||||
def do_edit_metadata_old(self, row_list, current_row):
|
||||
changed = set([])
|
||||
db = self.gui.library_view.model().db
|
||||
|
||||
while True:
|
||||
prev = next_ = None
|
||||
if current_row > 0:
|
||||
prev = db.title(row_list[current_row-1])
|
||||
if current_row < len(row_list) - 1:
|
||||
next_ = db.title(row_list[current_row+1])
|
||||
|
||||
d = MetadataSingleDialog(self.gui, row_list[current_row], db,
|
||||
prev=prev, next_=next_)
|
||||
d.view_format.connect(lambda
|
||||
fmt:self.gui.iactions['View'].view_format(row_list[current_row],
|
||||
fmt))
|
||||
ret = d.exec_()
|
||||
d.break_cycles()
|
||||
if ret != d.Accepted:
|
||||
break
|
||||
|
||||
changed.add(d.id)
|
||||
self.gui.library_view.model().refresh_ids(list(d.books_to_refresh))
|
||||
if d.row_delta == 0:
|
||||
break
|
||||
current_row += d.row_delta
|
||||
self.gui.library_view.set_current_row(current_row)
|
||||
self.gui.library_view.scroll_to_row(current_row)
|
||||
return changed, set()
|
||||
|
||||
def do_edit_metadata(self, row_list, current_row):
|
||||
from calibre.gui2.metadata.single import edit_metadata
|
||||
db = self.gui.library_view.model().db
|
||||
@ -613,6 +523,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
self.applied_ids, cr)
|
||||
if self.gui.cover_flow:
|
||||
self.gui.cover_flow.dataChanged()
|
||||
self.gui.tags_view.recount()
|
||||
|
||||
self.apply_id_map = []
|
||||
self.apply_pd = None
|
||||
|
@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2.preferences.main import Preferences
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.constants import DEBUG, isosx
|
||||
|
||||
class PreferencesAction(InterfaceAction):
|
||||
|
||||
@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction):
|
||||
|
||||
def genesis(self):
|
||||
pm = QMenu()
|
||||
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
|
||||
acname = _('Change calibre behavior') if isosx else _('Preferences')
|
||||
pm.addAction(QIcon(I('config.png')), acname, self.do_config)
|
||||
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
|
||||
self.gui.run_wizard)
|
||||
if not DEBUG:
|
||||
|
@ -60,7 +60,7 @@ class ViewAction(InterfaceAction):
|
||||
|
||||
def build_menus(self, db):
|
||||
self.view_menu.clear()
|
||||
self.view_menu.addAction(self.qaction)
|
||||
self.view_menu.addAction(self.view_action)
|
||||
self.view_menu.addAction(self.view_specific_action)
|
||||
self.view_menu.addSeparator()
|
||||
self.view_menu.addAction(self.action_pick_random)
|
||||
|
@ -62,8 +62,18 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||
|
||||
if isinstance(extra_customization_message, list):
|
||||
self.opt_extra_customization = []
|
||||
if len(extra_customization_message) > 6:
|
||||
row_func = lambda x, y: ((x/2) * 2) + y
|
||||
col_func = lambda x: x%2
|
||||
else:
|
||||
row_func = lambda x, y: x*2 + y
|
||||
col_func = lambda x: 0
|
||||
|
||||
for i, m in enumerate(extra_customization_message):
|
||||
label_text, tt = parse_msg(m)
|
||||
if not label_text:
|
||||
self.opt_extra_customization.append(None)
|
||||
continue
|
||||
if isinstance(settings.extra_customization[i], bool):
|
||||
self.opt_extra_customization.append(QCheckBox(label_text))
|
||||
self.opt_extra_customization[-1].setToolTip(tt)
|
||||
@ -75,8 +85,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||
l.setBuddy(self.opt_extra_customization[i])
|
||||
l.setWordWrap(True)
|
||||
self.opt_extra_customization[i].setText(settings.extra_customization[i])
|
||||
self.extra_layout.addWidget(l)
|
||||
self.extra_layout.addWidget(self.opt_extra_customization[i])
|
||||
self.extra_layout.addWidget(l, row_func(i, 0), col_func(i))
|
||||
self.extra_layout.addWidget(self.opt_extra_customization[i],
|
||||
row_func(i, 1), col_func(i))
|
||||
else:
|
||||
self.opt_extra_customization = QLineEdit()
|
||||
label_text, tt = parse_msg(extra_customization_message)
|
||||
@ -86,8 +97,8 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||
l.setWordWrap(True)
|
||||
if settings.extra_customization:
|
||||
self.opt_extra_customization.setText(settings.extra_customization)
|
||||
self.extra_layout.addWidget(l)
|
||||
self.extra_layout.addWidget(self.opt_extra_customization)
|
||||
self.extra_layout.addWidget(l, 0, 0)
|
||||
self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
|
||||
self.opt_save_template.setText(settings.save_template)
|
||||
|
||||
|
||||
|
@ -101,7 +101,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<layout class="QVBoxLayout" name="extra_layout"/>
|
||||
<layout class="QGridLayout" name="extra_layout"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
|
@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
|
||||
from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QAbstractItemView, QIcon,
|
||||
QDialogButtonBox, QFrame, QLabel, QTimer, QMenu, QApplication)
|
||||
|
||||
from calibre.ebooks.metadata import author_to_author_sort
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
|
||||
from calibre.utils.icu import sort_key, strcmp
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
class tableItem(QTableWidgetItem):
|
||||
def __ge__(self, other):
|
||||
@ -19,7 +20,7 @@ class tableItem(QTableWidgetItem):
|
||||
|
||||
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
|
||||
def __init__(self, parent, db, id_to_select):
|
||||
def __init__(self, parent, db, id_to_select, select_sort):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_EditAuthorsDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
@ -30,14 +31,23 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
|
||||
self.buttonBox.accepted.connect(self.accepted)
|
||||
|
||||
# Set up the column headings
|
||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.table.setColumnCount(2)
|
||||
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
|
||||
self.down_arrow_icon = QIcon(I('arrow-down.png'))
|
||||
self.up_arrow_icon = QIcon(I('arrow-up.png'))
|
||||
self.blank_icon = QIcon(I('blank.png'))
|
||||
self.auth_col = QTableWidgetItem(_('Author'))
|
||||
self.table.setHorizontalHeaderItem(0, self.auth_col)
|
||||
self.auth_col.setIcon(self.blank_icon)
|
||||
self.aus_col = QTableWidgetItem(_('Author sort'))
|
||||
self.table.setHorizontalHeaderItem(1, self.aus_col)
|
||||
self.aus_col.setIcon(self.up_arrow_icon)
|
||||
|
||||
# Add the data
|
||||
self.authors = {}
|
||||
auts = db.get_authors_with_ids()
|
||||
self.table.setRowCount(len(auts))
|
||||
setattr(self.table, '__lt__', lambda x, y: True if strcmp(x, y) < 0 else False)
|
||||
select_item = None
|
||||
for row, (id, author, sort) in enumerate(auts):
|
||||
author = author.replace('|', ',')
|
||||
@ -48,7 +58,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
self.table.setItem(row, 0, aut)
|
||||
self.table.setItem(row, 1, sort)
|
||||
if id == id_to_select:
|
||||
select_item = sort
|
||||
if select_sort:
|
||||
select_item = sort
|
||||
else:
|
||||
select_item = aut
|
||||
self.table.resizeColumnsToContents()
|
||||
|
||||
# set up the cellChanged signal only after the table is filled
|
||||
@ -69,23 +82,153 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort)
|
||||
self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author)
|
||||
|
||||
# Position on the desired item
|
||||
if select_item is not None:
|
||||
self.table.setCurrentItem(select_item)
|
||||
self.table.editItem(select_item)
|
||||
self.start_find_pos = select_item.row() * 2 + select_item.column()
|
||||
else:
|
||||
self.table.setCurrentCell(0, 0)
|
||||
self.start_find_pos = -1
|
||||
|
||||
# set up the search box
|
||||
self.find_box.initialize('manage_authors_search')
|
||||
self.find_box.lineEdit().returnPressed.connect(self.do_find)
|
||||
self.find_box.editTextChanged.connect(self.find_text_changed)
|
||||
self.find_button.clicked.connect(self.do_find)
|
||||
|
||||
l = QLabel(self.table)
|
||||
self.not_found_label = l
|
||||
l.setFrameStyle(QFrame.StyledPanel)
|
||||
l.setAutoFillBackground(True)
|
||||
l.setText(_('No matches found'))
|
||||
l.setAlignment(Qt.AlignVCenter)
|
||||
l.resize(l.sizeHint())
|
||||
l.move(10,20)
|
||||
l.setVisible(False)
|
||||
self.not_found_label.move(40, 40)
|
||||
self.not_found_label_timer = QTimer()
|
||||
self.not_found_label_timer.setSingleShot(True)
|
||||
self.not_found_label_timer.timeout.connect(
|
||||
self.not_found_label_timer_event, type=Qt.QueuedConnection)
|
||||
|
||||
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.table.customContextMenuRequested .connect(self.show_context_menu)
|
||||
|
||||
def show_context_menu(self, point):
|
||||
self.context_item = self.table.itemAt(point)
|
||||
case_menu = QMenu(_('Change Case'))
|
||||
action_upper_case = case_menu.addAction(_('Upper Case'))
|
||||
action_lower_case = case_menu.addAction(_('Lower Case'))
|
||||
action_swap_case = case_menu.addAction(_('Swap Case'))
|
||||
action_title_case = case_menu.addAction(_('Title Case'))
|
||||
action_capitalize = case_menu.addAction(_('Capitalize'))
|
||||
|
||||
action_upper_case.triggered.connect(self.upper_case)
|
||||
action_lower_case.triggered.connect(self.lower_case)
|
||||
action_swap_case.triggered.connect(self.swap_case)
|
||||
action_title_case.triggered.connect(self.title_case)
|
||||
action_capitalize.triggered.connect(self.capitalize)
|
||||
|
||||
m = self.au_context_menu = QMenu()
|
||||
ca = m.addAction(_('Copy'))
|
||||
ca.triggered.connect(self.copy_to_clipboard)
|
||||
ca = m.addAction(_('Paste'))
|
||||
ca.triggered.connect(self.paste_from_clipboard)
|
||||
m.addSeparator()
|
||||
|
||||
if self.context_item.column() == 0:
|
||||
ca = m.addAction(_('Copy to author sort'))
|
||||
ca.triggered.connect(self.copy_au_to_aus)
|
||||
else:
|
||||
ca = m.addAction(_('Copy to author'))
|
||||
ca.triggered.connect(self.copy_aus_to_au)
|
||||
m.addSeparator()
|
||||
m.addMenu(case_menu)
|
||||
m.exec_(self.table.mapToGlobal(point))
|
||||
|
||||
def copy_to_clipboard(self):
|
||||
cb = QApplication.clipboard()
|
||||
cb.setText(unicode(self.context_item.text()))
|
||||
|
||||
def paste_from_clipboard(self):
|
||||
cb = QApplication.clipboard()
|
||||
self.context_item.setText(cb.text())
|
||||
|
||||
def upper_case(self):
|
||||
self.context_item.setText(icu_upper(unicode(self.context_item.text())))
|
||||
|
||||
def lower_case(self):
|
||||
self.context_item.setText(icu_lower(unicode(self.context_item.text())))
|
||||
|
||||
def swap_case(self):
|
||||
self.context_item.setText(unicode(self.context_item.text()).swapcase())
|
||||
|
||||
def title_case(self):
|
||||
from calibre.utils.titlecase import titlecase
|
||||
self.context_item.setText(titlecase(unicode(self.context_item.text())))
|
||||
|
||||
def capitalize(self):
|
||||
from calibre.utils.icu import capitalize
|
||||
self.context_item.setText(capitalize(unicode(self.context_item.text())))
|
||||
|
||||
def copy_aus_to_au(self):
|
||||
row = self.context_item.row()
|
||||
dest = self.table.item(row, 0)
|
||||
dest.setText(self.context_item.text())
|
||||
|
||||
def copy_au_to_aus(self):
|
||||
row = self.context_item.row()
|
||||
dest = self.table.item(row, 1)
|
||||
dest.setText(self.context_item.text())
|
||||
|
||||
def not_found_label_timer_event(self):
|
||||
self.not_found_label.setVisible(False)
|
||||
|
||||
def find_text_changed(self):
|
||||
self.start_find_pos = -1
|
||||
|
||||
def do_find(self):
|
||||
self.not_found_label.setVisible(False)
|
||||
# For some reason the button box keeps stealing the RETURN shortcut.
|
||||
# Steal it back
|
||||
self.buttonBox.button(QDialogButtonBox.Ok).setDefault(False)
|
||||
self.buttonBox.button(QDialogButtonBox.Ok).setAutoDefault(False)
|
||||
self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(False)
|
||||
self.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
|
||||
st = icu_lower(unicode(self.find_box.currentText()))
|
||||
|
||||
for i in range(0, self.table.rowCount()*2):
|
||||
self.start_find_pos = (self.start_find_pos + 1) % (self.table.rowCount()*2)
|
||||
r = (self.start_find_pos/2)%self.table.rowCount()
|
||||
c = self.start_find_pos % 2
|
||||
item = self.table.item(r, c)
|
||||
text = icu_lower(unicode(item.text()))
|
||||
if st in text:
|
||||
self.table.setCurrentItem(item)
|
||||
self.table.setFocus(True)
|
||||
return
|
||||
# Nothing found. Pop up the little dialog for 1.5 seconds
|
||||
self.not_found_label.setVisible(True)
|
||||
self.not_found_label_timer.start(1500)
|
||||
|
||||
def do_sort_by_author(self):
|
||||
self.author_order = 1 if self.author_order == 0 else 0
|
||||
self.table.sortByColumn(0, self.author_order)
|
||||
self.sort_by_author.setChecked(True)
|
||||
self.sort_by_author_sort.setChecked(False)
|
||||
self.auth_col.setIcon(self.down_arrow_icon if self.author_order
|
||||
else self.up_arrow_icon)
|
||||
self.aus_col.setIcon(self.blank_icon)
|
||||
|
||||
def do_sort_by_author_sort(self):
|
||||
self.author_sort_order = 1 if self.author_sort_order == 0 else 0
|
||||
self.table.sortByColumn(1, self.author_sort_order)
|
||||
self.sort_by_author.setChecked(False)
|
||||
self.sort_by_author_sort.setChecked(True)
|
||||
self.aus_col.setIcon(self.down_arrow_icon if self.author_sort_order
|
||||
else self.up_arrow_icon)
|
||||
self.auth_col.setIcon(self.blank_icon)
|
||||
|
||||
def accepted(self):
|
||||
self.result = []
|
||||
|
@ -20,6 +20,50 @@
|
||||
<string>Manage authors</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="">
|
||||
<item>
|
||||
<widget class="QLabel">
|
||||
<property name="text">
|
||||
<string>&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>
|
||||
<widget class="QTableWidget" name="table">
|
||||
<property name="sizePolicy">
|
||||
@ -143,4 +187,11 @@ after changing Preferences->Advanced->Tweaks->Author sort name algorith
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>HistoryLineEdit</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre/gui2/widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
</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'
|
||||
|
||||
import os, shutil
|
||||
from contextlib import closing
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
||||
|
||||
from PyQt4.Qt import QDialog
|
||||
|
||||
from calibre.constants import isosx
|
||||
from calibre.gui2 import open_local_file
|
||||
from calibre.gui2 import open_local_file, error_dialog
|
||||
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
|
||||
from calibre.libunzip import extract as zipextract
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
||||
PersistentTemporaryFile)
|
||||
|
||||
class TweakEpub(QDialog, Ui_Dialog):
|
||||
'''
|
||||
@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog):
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
self.explode_button.clicked.connect(self.explode)
|
||||
self.rebuild_button.clicked.connect(self.rebuild)
|
||||
self.preview_button.clicked.connect(self.preview)
|
||||
|
||||
# Position update dialog overlaying top left of app window
|
||||
parent_loc = parent.pos()
|
||||
self.move(parent_loc.x(),parent_loc.y())
|
||||
|
||||
self.gui = parent
|
||||
self._preview_files = []
|
||||
|
||||
def cleanup(self):
|
||||
if isosx:
|
||||
try:
|
||||
@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog):
|
||||
# Delete directory containing exploded ePub
|
||||
if self._exploded is not None:
|
||||
shutil.rmtree(self._exploded, ignore_errors=True)
|
||||
for x in self._preview_files:
|
||||
try:
|
||||
os.remove(x)
|
||||
except:
|
||||
pass
|
||||
|
||||
def display_exploded(self):
|
||||
'''
|
||||
@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog):
|
||||
self.rebuild_button.setEnabled(True)
|
||||
self.explode_button.setEnabled(False)
|
||||
|
||||
def rebuild(self, *args):
|
||||
self._output = os.path.join(self._exploded, 'rebuilt.epub')
|
||||
with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
|
||||
def do_rebuild(self, src):
|
||||
with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf:
|
||||
# Write mimetype
|
||||
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
|
||||
# Write everything else
|
||||
@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog):
|
||||
zfn = os.path.relpath(absfn,
|
||||
self._exploded).replace(os.sep, '/')
|
||||
zf.write(absfn, zfn)
|
||||
|
||||
def preview(self):
|
||||
if not self._exploded:
|
||||
return error_dialog(self, _('Cannot preview'),
|
||||
_('You must first explode the epub before previewing.'),
|
||||
show=True)
|
||||
|
||||
tf = PersistentTemporaryFile('.epub')
|
||||
tf.close()
|
||||
self._preview_files.append(tf.name)
|
||||
|
||||
self.do_rebuild(tf.name)
|
||||
|
||||
self.gui.iactions['View']._view_file(tf.name)
|
||||
|
||||
def rebuild(self, *args):
|
||||
self._output = os.path.join(self._exploded, 'rebuilt.epub')
|
||||
self.do_rebuild(self._output)
|
||||
return QDialog.accept(self)
|
||||
|
||||
|
@ -23,6 +23,16 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><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">
|
||||
<widget class="QPushButton" name="explode_button">
|
||||
<property name="statusTip">
|
||||
@ -37,23 +47,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="rebuild_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="statusTip">
|
||||
<string>Rebuild ePub from exploded contents</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Rebuild ePub</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="cancel_button">
|
||||
<property name="statusTip">
|
||||
@ -68,13 +61,31 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><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>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="rebuild_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
<property name="statusTip">
|
||||
<string>Rebuild ePub from exploded contents</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&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>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{
|
||||
receiver = partial(self._location_selected, name)
|
||||
ac.triggered.connect(receiver)
|
||||
self.tooltips[name] = tooltip
|
||||
|
||||
m = QMenu(parent)
|
||||
self._mem.append(m)
|
||||
a = m.addAction(icon, tooltip)
|
||||
a.triggered.connect(receiver)
|
||||
if name != 'library':
|
||||
m = QMenu(parent)
|
||||
self._mem.append(m)
|
||||
a = m.addAction(icon, tooltip)
|
||||
a.triggered.connect(receiver)
|
||||
self._mem.append(a)
|
||||
a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
|
||||
a.triggered.connect(self._eject_requested)
|
||||
ac.setMenu(m)
|
||||
self._mem.append(a)
|
||||
else:
|
||||
ac.setToolTip(tooltip)
|
||||
ac.setMenu(m)
|
||||
ac.calibre_name = name
|
||||
|
||||
return ac
|
||||
@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{
|
||||
|
||||
def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
|
||||
switch_actions, choose_action):
|
||||
self.switch_menu = 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.cs_menus = []
|
||||
for t, acs in [(_('Quick switch'), quick_actions),
|
||||
@ -85,7 +91,9 @@ class LocationManager(QObject): # {{{
|
||||
self.switch_menu.addSeparator()
|
||||
for ac in switch_actions:
|
||||
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):
|
||||
if location != self.current_location and hasattr(self,
|
||||
|
@ -439,10 +439,16 @@ class BooksView(QTableView): # {{{
|
||||
|
||||
if tweaks['sort_columns_at_startup'] is not None:
|
||||
sh = []
|
||||
for c,d in tweaks['sort_columns_at_startup']:
|
||||
if not isinstance(d, bool):
|
||||
d = True if d == 0 else False
|
||||
sh.append((c, d))
|
||||
try:
|
||||
for c,d in tweaks['sort_columns_at_startup']:
|
||||
if not isinstance(d, bool):
|
||||
d = True if d == 0 else False
|
||||
sh.append((c, d))
|
||||
except:
|
||||
# Ignore invalid tweak values as users seem to often get them
|
||||
# wrong
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
old_state['sort_history'] = sh
|
||||
|
||||
self.apply_state(old_state)
|
||||
|
@ -299,13 +299,13 @@ def run_gui(opts, args, actions, listener, app, gui_debug=None):
|
||||
if getattr(runner.main, 'debug_on_restart', False):
|
||||
run_in_debug_mode()
|
||||
else:
|
||||
import subprocess
|
||||
print 'Restarting with:', e, sys.argv
|
||||
if hasattr(sys, 'frameworks_dir'):
|
||||
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
|
||||
import subprocess
|
||||
subprocess.Popen('sleep 3s; open '+app, shell=True)
|
||||
else:
|
||||
os.execvp(e, sys.argv)
|
||||
subprocess.Popen([e] + sys.argv[1:])
|
||||
else:
|
||||
if iswindows:
|
||||
try:
|
||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap, re, os
|
||||
|
||||
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal,
|
||||
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal, QMessageBox,
|
||||
QIcon, QToolButton, QWidget, QLabel, QGridLayout,
|
||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
|
||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
|
||||
@ -19,10 +19,10 @@ from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import tweaks, prefs
|
||||
from calibre.ebooks.metadata import (title_sort, authors_to_string,
|
||||
string_to_authors, check_isbn)
|
||||
string_to_authors, check_isbn, authors_to_sort_string)
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE,
|
||||
choose_files, error_dialog, choose_images, question_dialog)
|
||||
choose_files, error_dialog, choose_images)
|
||||
from calibre.utils.date import local_tz, qt_to_dt
|
||||
from calibre import strftime
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
@ -31,6 +31,16 @@ from calibre.utils.date import utcfromtimestamp
|
||||
from calibre.gui2.comments_editor import Editor
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
from calibre.utils.icu import strcmp
|
||||
|
||||
def save_dialog(parent, title, msg, det_msg=''):
|
||||
d = QMessageBox(parent)
|
||||
d.setWindowTitle(title)
|
||||
d.setText(msg)
|
||||
d.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
|
||||
return d.exec_()
|
||||
|
||||
|
||||
|
||||
'''
|
||||
The interface common to all widgets used to set basic metadata
|
||||
@ -156,7 +166,7 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Author(s):')
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent, manage_authors):
|
||||
self.dialog = parent
|
||||
self.books_to_refresh = set([])
|
||||
MultiCompleteComboBox.__init__(self, parent)
|
||||
@ -164,6 +174,28 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
self.setEditable(True)
|
||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||
manage_authors.triggered.connect(self.manage_authors)
|
||||
|
||||
def manage_authors(self):
|
||||
if self.original_val != self.current_val:
|
||||
d = save_dialog(self, _('Authors changed'),
|
||||
_('You have changed the authors for this book. You must save '
|
||||
'these changes before you can use Manage authors. Do you '
|
||||
'want to save these changes?'))
|
||||
if d == QMessageBox.Cancel:
|
||||
return
|
||||
if d == QMessageBox.Yes:
|
||||
self.commit(self.db, self.id_)
|
||||
self.db.commit()
|
||||
self.original_val = self.current_val
|
||||
else:
|
||||
self.current_val = self.original_val
|
||||
first_author = self.current_val[0] if len(self.current_val) else None
|
||||
first_author_id = self.db.get_author_id(first_author) if first_author else None
|
||||
self.dialog.parent().do_author_sort_edit(self, first_author_id,
|
||||
select_sort=False)
|
||||
self.initialize(self.db, self.id_)
|
||||
self.dialog.author_sort.initialize(self.db, self.id_)
|
||||
|
||||
def get_default(self):
|
||||
return _('Unknown')
|
||||
@ -175,8 +207,8 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
self.clear()
|
||||
for i in all_authors:
|
||||
id, name = i
|
||||
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
||||
self.addItem(authors_to_string(name))
|
||||
name = name.strip().replace('|', ',')
|
||||
self.addItem(name)
|
||||
|
||||
self.set_separator('&')
|
||||
self.set_space_before_sep(True)
|
||||
@ -188,6 +220,8 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
au = _('Unknown')
|
||||
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||
self.original_val = self.current_val
|
||||
self.id_ = id_
|
||||
self.db = db
|
||||
|
||||
def commit(self, db, id_):
|
||||
authors = self.current_val
|
||||
@ -238,7 +272,7 @@ class AuthorSortEdit(EnLineEdit):
|
||||
'No action is required if this is what you want.'))
|
||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
||||
|
||||
self.authors_edit.editTextChanged.connect(self.update_state)
|
||||
self.authors_edit.editTextChanged.connect(self.update_state_and_val)
|
||||
self.textChanged.connect(self.update_state)
|
||||
|
||||
autogen_button.clicked.connect(self.auto_generate)
|
||||
@ -260,12 +294,19 @@ class AuthorSortEdit(EnLineEdit):
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def update_state_and_val(self):
|
||||
# Handle case change if the authors box changed
|
||||
aus = authors_to_sort_string(self.authors_edit.current_val)
|
||||
if strcmp(aus, self.current_val) == 0:
|
||||
self.current_val = aus
|
||||
self.update_state()
|
||||
|
||||
def update_state(self, *args):
|
||||
au = unicode(self.authors_edit.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
au = self.db.author_sort_from_authors(string_to_authors(au))
|
||||
|
||||
normal = au == self.current_val
|
||||
normal = strcmp(au, self.current_val) == 0
|
||||
if normal:
|
||||
col = 'rgb(0, 255, 0, 20%)'
|
||||
else:
|
||||
@ -900,10 +941,13 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
|
||||
|
||||
def edit(self, db, id_):
|
||||
if self.changed:
|
||||
if question_dialog(self, _('Tags changed'),
|
||||
d = save_dialog(self, _('Tags changed'),
|
||||
_('You have changed the tags. In order to use the tags'
|
||||
' editor, you must either discard or apply these '
|
||||
'changes. Apply changes?'), show_copy_button=False):
|
||||
'changes. Apply changes?'))
|
||||
if d == QMessageBox.Cancel:
|
||||
return
|
||||
if d == QMessageBox.Yes:
|
||||
self.commit(db, id_)
|
||||
db.commit()
|
||||
self.original_val = self.current_val
|
||||
|
@ -1,308 +1,195 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import with_statement
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback
|
||||
from threading import Thread
|
||||
from Queue import Queue, Empty
|
||||
from functools import partial
|
||||
from itertools import izip
|
||||
from threading import Event
|
||||
|
||||
from PyQt4.Qt import QObject, QTimer, QDialog, \
|
||||
QVBoxLayout, QTextBrowser, QLabel, QGroupBox, QDialogButtonBox
|
||||
from PyQt4.Qt import (QIcon, QDialog,
|
||||
QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
|
||||
|
||||
from calibre.ebooks.metadata.fetch import search, get_social_metadata
|
||||
from calibre.gui2 import config, error_dialog
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.ebooks.metadata.covers import download_cover
|
||||
from calibre.customize.ui import get_isbndb_key
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
from calibre.ebooks.metadata.sources.identify import identify, msprefs
|
||||
from calibre.ebooks.metadata.sources.covers import download_cover
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.customize.ui import metadata_plugins
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class Worker(Thread):
|
||||
'Cover downloader'
|
||||
# Start download {{{
|
||||
def show_config(gui, parent):
|
||||
from calibre.gui2.preferences import show_config_widget
|
||||
show_config_widget('Sharing', 'Metadata download', parent=parent,
|
||||
gui=gui, never_shutdown=True)
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.jobs = Queue()
|
||||
self.results = Queue()
|
||||
class ConfirmDialog(QDialog):
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
id, mi = self.jobs.get()
|
||||
if not getattr(mi, 'isbn', False):
|
||||
break
|
||||
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:
|
||||
cdata, errors = download_cover(mi)
|
||||
if cdata:
|
||||
self.results.put((id, mi, True, cdata))
|
||||
else:
|
||||
msg = []
|
||||
for e in errors:
|
||||
if not e[0]:
|
||||
msg.append(e[-1] + ' - ' + e[1])
|
||||
self.results.put((id, mi, False, '\n'.join(msg)))
|
||||
results = identify(log, Event(), title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
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
|
||||
if not self.downloader.is_alive():
|
||||
while True:
|
||||
try:
|
||||
r = self.downloader.results.get_nowait()
|
||||
self.handle_result(r)
|
||||
except Empty:
|
||||
break
|
||||
self.pd.accept()
|
||||
return
|
||||
except:
|
||||
self.cancel()
|
||||
raise
|
||||
QTimer.singleShot(50, self.do_one)
|
||||
|
||||
def handle_result(self, r):
|
||||
id_, typ, ok, title = r
|
||||
what = _('cover') if typ == 'cover' else _('metadata')
|
||||
which = _('Downloaded') if ok else _('Failed to get')
|
||||
if self.get_covers or typ != 'cover' or ok:
|
||||
# Do not show message when cover fetch fails if user didn't ask to
|
||||
# download covers
|
||||
self.pd.set_msg(_('%s %s for: %s') % (which, what, title))
|
||||
self.pd.value += 1
|
||||
if ok:
|
||||
self.updated.add(id_)
|
||||
if typ == 'cover':
|
||||
try:
|
||||
self.db.set_cover(id_,
|
||||
self.downloader.fetched_covers.pop(id_))
|
||||
except:
|
||||
self.downloader.cover_failures[id_] = \
|
||||
traceback.format_exc()
|
||||
if results:
|
||||
all_failed = False
|
||||
mi = merge_result(mi, results[0])
|
||||
identifiers = mi.identifiers
|
||||
if not mi.is_null('rating'):
|
||||
# set_metadata expects a rating out of 10
|
||||
mi.rating *= 2
|
||||
else:
|
||||
try:
|
||||
self.set_metadata(id_)
|
||||
except:
|
||||
self.downloader.failures[id_] = \
|
||||
traceback.format_exc()
|
||||
|
||||
def set_metadata(self, id_):
|
||||
mi = self.downloader.metadata[id_]
|
||||
if self.downloader.set_metadata:
|
||||
self.db.set_metadata(id_, mi)
|
||||
if not self.downloader.set_metadata and self.downloader.get_social_metadata:
|
||||
if mi.rating:
|
||||
self.db.set_rating(id_, mi.rating)
|
||||
if mi.tags:
|
||||
self.db.set_tags(id_, mi.tags)
|
||||
if mi.comments:
|
||||
self.db.set_comment(id_, mi.comments)
|
||||
if mi.series:
|
||||
self.db.set_series(id_, mi.series)
|
||||
if mi.series_index is not None:
|
||||
self.db.set_series_index(id_, mi.series_index)
|
||||
|
||||
def show_report(self):
|
||||
f, cf = self.downloader.failures, self.downloader.cover_failures
|
||||
report = []
|
||||
if f:
|
||||
report.append(
|
||||
'<h3>Failed to download metadata for the following:</h3><ol>')
|
||||
for id_, err in f.items():
|
||||
mi = self.downloader.metadata[id_]
|
||||
report.append('<li><b>%s</b><pre>%s</pre></li>' % (mi.title,
|
||||
unicode(err)))
|
||||
report.append('</ol>')
|
||||
if cf:
|
||||
report.append(
|
||||
'<h3>Failed to download cover for the following:</h3><ol>')
|
||||
for id_, err in cf.items():
|
||||
mi = self.downloader.metadata[id_]
|
||||
report.append('<li><b>%s</b><pre>%s</pre></li>' % (mi.title,
|
||||
unicode(err)))
|
||||
report.append('</ol>')
|
||||
|
||||
if len(self.updated) != self.total or report:
|
||||
d = QDialog(self.parent())
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Ok, parent=d)
|
||||
v1 = QVBoxLayout()
|
||||
d.setLayout(v1)
|
||||
d.setWindowTitle(_('Done'))
|
||||
v1.addWidget(QLabel(_('Successfully downloaded metadata for %d out of %d books') %
|
||||
(len(self.updated), self.total)))
|
||||
gb = QGroupBox(_('Details'), self.parent())
|
||||
v2 = QVBoxLayout()
|
||||
gb.setLayout(v2)
|
||||
b = QTextBrowser(self.parent())
|
||||
v2.addWidget(b)
|
||||
b.setHtml('\n'.join(report))
|
||||
v1.addWidget(gb)
|
||||
v1.addWidget(bb)
|
||||
bb.accepted.connect(d.accept)
|
||||
d.resize(800, 600)
|
||||
d.exec_()
|
||||
|
||||
log.error('Failed to download metadata for', title)
|
||||
failed_ids.add(i)
|
||||
# We don't want set_metadata operating on anything but covers
|
||||
mi = merge_result(mi, mi)
|
||||
if covers:
|
||||
cdata = download_cover(log, title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
if cdata is not None:
|
||||
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
|
||||
f.write(cdata[-1])
|
||||
mi.cover = f.name
|
||||
all_failed = False
|
||||
else:
|
||||
failed_covers.add(i)
|
||||
ans[i] = mi
|
||||
count += 1
|
||||
notifications.put((count/len(ids),
|
||||
_('Downloaded %d of %d')%(count, len(ids))))
|
||||
log('Download complete, with %d failures'%len(failed_ids))
|
||||
return (ans, failed_ids, failed_covers, title_map, all_failed)
|
||||
|
||||
|
||||
|
||||
|
@ -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.deduce_author_sort_button = b = QToolButton(self)
|
||||
b.setToolTip(_(
|
||||
'Automatically create the author sort entry based on the current'
|
||||
' author entry.\n'
|
||||
'Using this button to create author sort will change author sort from'
|
||||
' red to green.'))
|
||||
b.setToolTip('<p>' +
|
||||
_('Automatically create the author sort entry based on the current '
|
||||
'author entry. Using this button to create author sort will '
|
||||
'change author sort from red to green. There is a menu of '
|
||||
'functions available under this button. Click and hold '
|
||||
'on the button to see it.') + '</p>')
|
||||
b.m = m = QMenu()
|
||||
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
||||
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
||||
ac3 = m.addAction(QIcon(I('user_profile.png')), _('Manage authors'))
|
||||
b.setMenu(m)
|
||||
self.authors = AuthorsEdit(self)
|
||||
self.authors = AuthorsEdit(self, ac3)
|
||||
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||
ac2)
|
||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||
@ -198,7 +200,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
ans = self.custom_metadata_widgets
|
||||
for i in range(len(ans)-1):
|
||||
if before is not None and i == 0:
|
||||
pass# Do something
|
||||
pass
|
||||
if len(ans[i+1].widgets) == 2:
|
||||
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||
else:
|
||||
@ -206,7 +208,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
for c in range(2, len(ans[i].widgets), 2):
|
||||
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||
if after is not None:
|
||||
pass # Do something
|
||||
pass
|
||||
# }}}
|
||||
|
||||
def do_view_format(self, path, fmt):
|
||||
@ -290,13 +292,17 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
show=True)
|
||||
return
|
||||
|
||||
def update_from_mi(self, mi):
|
||||
def update_from_mi(self, mi, update_sorts=True):
|
||||
if not mi.is_null('title'):
|
||||
self.title.current_val = mi.title
|
||||
if update_sorts:
|
||||
self.title_sort.auto_generate()
|
||||
if not mi.is_null('authors'):
|
||||
self.authors.current_val = mi.authors
|
||||
if not mi.is_null('author_sort'):
|
||||
self.author_sort.current_val = mi.author_sort
|
||||
elif update_sorts:
|
||||
self.author_sort.auto_generate()
|
||||
if not mi.is_null('rating'):
|
||||
try:
|
||||
self.rating.current_val = mi.rating
|
||||
@ -728,7 +734,135 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
|
||||
class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{
|
||||
|
||||
cc_two_column = False
|
||||
one_line_comments_toolbar = True
|
||||
|
||||
def do_layout(self):
|
||||
self.central_widget.clear()
|
||||
self.labels = []
|
||||
sto = QWidget.setTabOrder
|
||||
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
tab0 = QWidget(self)
|
||||
self.central_widget.addTab(tab0, _("&Metadata"))
|
||||
l = QGridLayout()
|
||||
tab0.setLayout(l)
|
||||
|
||||
# Basic metadata in col 0
|
||||
tl = QGridLayout()
|
||||
gb = QGroupBox(_('Basic metadata'), tab0)
|
||||
l.addWidget(gb, 0, 0, 1, 1)
|
||||
gb.setLayout(tl)
|
||||
|
||||
self.button_box.addButton(self.fetch_metadata_button,
|
||||
QDialogButtonBox.ActionRole)
|
||||
self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.config_metadata_button.setText(_('Configure metadata downloading'))
|
||||
self.button_box.addButton(self.config_metadata_button,
|
||||
QDialogButtonBox.ActionRole)
|
||||
sto(self.button_box, self.title)
|
||||
|
||||
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
|
||||
ql = BuddyLabel(widget)
|
||||
tl.addWidget(ql, row, 1, 1, 1)
|
||||
tl.addWidget(widget, row, 2, 1, 1)
|
||||
if button is not None:
|
||||
tl.addWidget(button, row, 3, span, 1)
|
||||
if icon is not None:
|
||||
button.setIcon(QIcon(I(icon)))
|
||||
if tab_to is not None:
|
||||
if button is not None:
|
||||
sto(widget, button)
|
||||
sto(button, tab_to)
|
||||
else:
|
||||
sto(widget, tab_to)
|
||||
|
||||
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||
|
||||
create_row(0, self.title, self.title_sort,
|
||||
button=self.deduce_title_sort_button, span=2,
|
||||
icon='auto_author_sort.png')
|
||||
create_row(1, self.title_sort, self.authors)
|
||||
create_row(2, self.authors, self.author_sort,
|
||||
button=self.deduce_author_sort_button,
|
||||
span=2, icon='auto_author_sort.png')
|
||||
create_row(3, self.author_sort, self.series)
|
||||
create_row(4, self.series, self.series_index,
|
||||
button=self.remove_unused_series_button, icon='trash.png')
|
||||
create_row(5, self.series_index, self.tags)
|
||||
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
|
||||
create_row(7, self.rating, self.pubdate)
|
||||
create_row(8, self.pubdate, self.publisher,
|
||||
button=self.pubdate.clear_button, icon='trash.png')
|
||||
create_row(9, self.publisher, self.timestamp)
|
||||
create_row(10, self.timestamp, self.identifiers,
|
||||
button=self.timestamp.clear_button, icon='trash.png')
|
||||
create_row(11, self.identifiers, self.comments,
|
||||
button=self.clear_identifiers_button, icon='trash.png')
|
||||
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
|
||||
12, 1, 1 ,1)
|
||||
|
||||
# Custom metadata in col 1
|
||||
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||
if w is not None:
|
||||
gb = QGroupBox(_('Custom metadata'), tab0)
|
||||
gbl = QVBoxLayout()
|
||||
gb.setLayout(gbl)
|
||||
sr = QScrollArea(gb)
|
||||
sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
sr.setWidgetResizable(True)
|
||||
sr.setBackgroundRole(QPalette.Base)
|
||||
sr.setFrameStyle(QFrame.NoFrame)
|
||||
sr.setWidget(w)
|
||||
gbl.addWidget(sr)
|
||||
l.addWidget(gb, 0, 1, 1, 1)
|
||||
sp = QSizePolicy()
|
||||
sp.setVerticalStretch(10)
|
||||
sp.setHorizontalPolicy(QSizePolicy.Fixed)
|
||||
sp.setVerticalPolicy(QSizePolicy.Expanding)
|
||||
gb.setSizePolicy(sp)
|
||||
self.set_custom_metadata_tab_order()
|
||||
|
||||
# comments span col 0 & 1
|
||||
w = QGroupBox(_('Comments'), tab0)
|
||||
sp = QSizePolicy()
|
||||
sp.setVerticalStretch(10)
|
||||
sp.setHorizontalPolicy(QSizePolicy.Expanding)
|
||||
sp.setVerticalPolicy(QSizePolicy.Expanding)
|
||||
w.setSizePolicy(sp)
|
||||
lb = QHBoxLayout()
|
||||
w.setLayout(lb)
|
||||
lb.addWidget(self.comments)
|
||||
l.addWidget(w, 1, 0, 1, 2)
|
||||
|
||||
# Cover & formats in col 3
|
||||
gb = QGroupBox(_('Cover'), tab0)
|
||||
lb = QGridLayout()
|
||||
gb.setLayout(lb)
|
||||
lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter)
|
||||
sto(self.clear_identifiers_button, self.cover.buttons[0])
|
||||
for i, b in enumerate(self.cover.buttons[:3]):
|
||||
lb.addWidget(b, 1, i, 1, 1)
|
||||
sto(b, self.cover.buttons[i+1])
|
||||
hl = QHBoxLayout()
|
||||
for b in self.cover.buttons[3:]:
|
||||
hl.addWidget(b)
|
||||
sto(self.cover.buttons[-2], self.cover.buttons[-1])
|
||||
lb.addLayout(hl, 2, 0, 1, 3)
|
||||
l.addWidget(gb, 0, 2, 1, 1)
|
||||
l.addWidget(self.formats_manager, 1, 2, 1, 1)
|
||||
sto(self.cover.buttons[-1], self.formats_manager)
|
||||
|
||||
self.formats_manager.formats.setMaximumWidth(10000)
|
||||
self.formats_manager.formats.setIconSize(QSize(32, 32))
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1,
|
||||
'alt2': MetadataSingleDialogAlt2}
|
||||
|
||||
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
|
||||
set_current_callback=None):
|
||||
|
@ -19,7 +19,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.oeb.iterator import is_supported
|
||||
from calibre.constants import iswindows
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import test_eight_code
|
||||
|
||||
class OutputFormatSetting(Setting):
|
||||
|
||||
@ -40,12 +39,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
r('network_timeout', prefs)
|
||||
|
||||
|
||||
r('overwrite_author_title_metadata', config)
|
||||
r('get_social_metadata', config)
|
||||
if test_eight_code:
|
||||
self.opt_overwrite_author_title_metadata.setVisible(False)
|
||||
self.opt_get_social_metadata.setVisible(False)
|
||||
r('new_version_notification', config)
|
||||
r('upload_news_to_device', config)
|
||||
r('delete_news_from_library_on_upload', config)
|
||||
@ -67,13 +60,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
signal.connect(self.internally_viewed_formats_changed)
|
||||
|
||||
r('bools_are_tristate', db.prefs, restart_required=True)
|
||||
if test_eight_code:
|
||||
r = self.register
|
||||
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
|
||||
r('edit_metadata_single_layout', gprefs, choices=choices)
|
||||
else:
|
||||
self.opt_edit_metadata_single_layout.setVisible(False)
|
||||
self.edit_metadata_single_label.setVisible(False)
|
||||
r = self.register
|
||||
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1'),
|
||||
(_('All on 1 tab'), 'alt2')]
|
||||
r('edit_metadata_single_layout', gprefs, choices=choices)
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
|
@ -14,41 +14,14 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_overwrite_author_title_metadata">
|
||||
<property name="text">
|
||||
<string>&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">
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="opt_new_version_notification">
|
||||
<property name="text">
|
||||
<string>Show notification when &new version is available</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
||||
<property name="toolTip">
|
||||
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
|
||||
@ -59,21 +32,21 @@ If not checked, the values can be Yes or No.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="opt_upload_news_to_device">
|
||||
<property name="text">
|
||||
<string>Automatically send downloaded &news to ebook reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
|
||||
<property name="text">
|
||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="5" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_23">
|
||||
@ -97,7 +70,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<item row="5" column="1">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
@ -130,7 +103,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="7" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="priority_label">
|
||||
@ -169,7 +142,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="8" column="2">
|
||||
<item row="7" column="1">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_170">
|
||||
@ -202,7 +175,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<item row="8" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="edit_metadata_single_label">
|
||||
@ -223,7 +196,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="20" column="0">
|
||||
<item row="19" column="0">
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title">
|
||||
<string>Preferred &input format order:</string>
|
||||
@ -285,7 +258,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="20" column="2">
|
||||
<item row="19" column="1">
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Use internal &viewer for:</string>
|
||||
@ -304,7 +277,7 @@ If not checked, the values can be Yes or No.</string>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="2">
|
||||
<item row="8" column="1">
|
||||
<widget class="QPushButton" name="reset_confirmation_button">
|
||||
<property name="text">
|
||||
<string>Reset all disabled &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
|
||||
|
||||
def restore_defaults(self):
|
||||
self.overrides = dict([(f, self.state(f, True)) for f in self.fields])
|
||||
self.overrides = dict([(f, self.state(f, Qt.Checked)) for f in self.fields])
|
||||
self.reset()
|
||||
|
||||
def select_all(self):
|
||||
self.overrides = dict([(f, Qt.Checked) for f in self.fields])
|
||||
self.reset()
|
||||
|
||||
def clear_all(self):
|
||||
self.overrides = dict([(f, Qt.Unchecked) for f in self.fields])
|
||||
self.reset()
|
||||
|
||||
def setData(self, index, val, role):
|
||||
@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.fields_view.setModel(self.fields_model)
|
||||
self.fields_model.dataChanged.connect(self.changed_signal)
|
||||
|
||||
self.select_all_button.clicked.connect(self.fields_model.select_all)
|
||||
self.clear_all_button.clicked.connect(self.fields_model.clear_all)
|
||||
|
||||
def configure_plugin(self):
|
||||
for index in self.sources_view.selectionModel().selectedRows():
|
||||
plugin = self.sources_model.data(index, Qt.UserRole)
|
||||
|
@ -77,8 +77,8 @@
|
||||
<property name="title">
|
||||
<string>Downloaded metadata fields</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QListView" name="fields_view">
|
||||
<property name="toolTip">
|
||||
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
|
||||
@ -88,6 +88,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="select_all_button">
|
||||
<property name="text">
|
||||
<string>&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>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -30,6 +30,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
r('enforce_cpu_limit', config, restart_required=True)
|
||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||
self.button_open_config_dir.clicked.connect(self.open_config_dir)
|
||||
self.user_defined_device_button.clicked.connect(self.user_defined_device)
|
||||
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
||||
self.button_osx_symlinks.setVisible(isosx)
|
||||
|
||||
@ -38,6 +39,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
d = DebugDevice(self)
|
||||
d.exec_()
|
||||
|
||||
def user_defined_device(self, *args):
|
||||
from calibre.gui2.preferences.device_user_defined import UserDefinedDevice
|
||||
d = UserDefinedDevice(self)
|
||||
d.exec_()
|
||||
|
||||
def open_config_dir(self, *args):
|
||||
from calibre.utils.config import config_dir
|
||||
open_local_file(config_dir)
|
||||
|
@ -58,7 +58,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="user_defined_device_button">
|
||||
<property name="text">
|
||||
<string>Get information to setup the &user defined device</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<spacer name="verticalSpacer_6">
|
||||
<property name="orientation">
|
||||
<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):
|
||||
from calibre.gui2 import JSONConfig
|
||||
|
||||
|
||||
self.gui = gui
|
||||
self.name = name
|
||||
self.base_plugin = None
|
||||
@ -79,14 +79,14 @@ class StorePlugin(object): # {{{
|
||||
return items as a generator.
|
||||
|
||||
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)
|
||||
isn't available because the store does not display cover images then it's okay to
|
||||
ignore it.
|
||||
|
||||
|
||||
At the very least a :class:`calibre.gui2.store.search_result.SearchResult`
|
||||
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
|
||||
: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.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def get_details(self, search_result, timeout=60):
|
||||
'''
|
||||
Delayed search for information about specific search items.
|
||||
|
||||
|
||||
Typically, this will be used when certain information such as
|
||||
formats, drm status, cover url are not part of the main search
|
||||
results and the information is on another web page.
|
||||
|
||||
|
||||
Using this function allows for the main information (title, author)
|
||||
to be displayed in the search results while other information can
|
||||
take extra time to load. Splitting retrieving data that takes longer
|
||||
to load into a separate function will give the illusion of the search
|
||||
being faster.
|
||||
|
||||
|
||||
:param search_result: A search result that need details set.
|
||||
:param timeout: The maximum amount of time in seconds to spend downloading details.
|
||||
|
||||
|
||||
:return: True if the search_result was modified otherwise False
|
||||
'''
|
||||
return False
|
||||
@ -133,30 +133,30 @@ class StorePlugin(object): # {{{
|
||||
is called to update the caches. It is recommended to call this function
|
||||
from :meth:`open`. Especially if :meth:`open` does anything other than
|
||||
open a web page.
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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 timeout: The maximum amount of time that should be spent in
|
||||
any given network connection.
|
||||
|
||||
|
||||
:param force: Force updating the cache even if the plugin has determined
|
||||
it is not necessary.
|
||||
|
||||
|
||||
:param suppress_progress: Should a progress indicator be shown.
|
||||
|
||||
|
||||
:return: True if the cache was updated, False otherwise.
|
||||
'''
|
||||
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['sort_col'] = self.results_view.model().sort_col
|
||||
self.config['sort_order'] = self.results_view.model().sort_order
|
||||
self.config['open_external'] = self.open_external.isChecked()
|
||||
|
||||
store_check = {}
|
||||
for n in self.store_plugins:
|
||||
@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
else:
|
||||
self.resize_columns()
|
||||
|
||||
self.open_external.setChecked(self.config.get('open_external', False))
|
||||
|
||||
store_check = self.config.get('store_checked', None)
|
||||
if store_check:
|
||||
for n in store_check:
|
||||
@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_result(index)
|
||||
self.store_plugins[result.store_name].open(self, result.detail_item)
|
||||
self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
|
||||
|
||||
def check_progress(self):
|
||||
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
|
||||
|
@ -70,7 +70,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>215</width>
|
||||
<height>116</height>
|
||||
<height>93</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
@ -101,6 +101,16 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="open_external">
|
||||
<property name="toolTip">
|
||||
<string>Open a selected book in the system's web browser</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Open in &external browser</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QSplitter" name="splitter_2">
|
||||
|
@ -2048,12 +2048,12 @@ class TagBrowserMixin(object): # {{{
|
||||
self.library_view.select_rows(ids)
|
||||
# refreshing the tags view happens at the emit()/call() site
|
||||
|
||||
def do_author_sort_edit(self, parent, id):
|
||||
def do_author_sort_edit(self, parent, id, select_sort=True):
|
||||
'''
|
||||
Open the manage authors dialog
|
||||
'''
|
||||
db = self.library_view.model().db
|
||||
editor = EditAuthorsDialog(parent, db, id)
|
||||
editor = EditAuthorsDialog(parent, db, id, select_sort)
|
||||
d = editor.exec_()
|
||||
if d:
|
||||
for (id, old_author, new_author, new_sort) in editor.result:
|
||||
|
@ -8,6 +8,7 @@ from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from xml.sax.saxutils import escape
|
||||
from lxml import etree
|
||||
from types import StringType, UnicodeType
|
||||
|
||||
from calibre import prints, prepare_string_for_xml, strftime
|
||||
from calibre.constants import preferred_encoding, DEBUG
|
||||
@ -15,13 +16,16 @@ from calibre.customize import CatalogPlugin
|
||||
from calibre.customize.conversion import OptionRecommendation, DummyReporter
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
|
||||
from calibre.ebooks.chardet import substitute_entites
|
||||
from calibre.library.save_to_disk import preprocess_template
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.bibtex import BibTeX
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf
|
||||
from calibre.utils.html2text import html2text
|
||||
from calibre.utils.icu import capitalize
|
||||
from calibre.utils.logging import default_log as log
|
||||
from calibre.utils.zipfile import ZipFile, ZipInfo
|
||||
from calibre.utils.magick.draw import thumbnail
|
||||
from calibre.utils.zipfile import ZipFile, ZipInfo
|
||||
|
||||
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
|
||||
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
|
||||
@ -303,12 +307,6 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
|
||||
def run(self, path_to_output, opts, db, notification=DummyReporter()):
|
||||
|
||||
from types import StringType, UnicodeType
|
||||
|
||||
from calibre.library.save_to_disk import preprocess_template
|
||||
#Bibtex functions
|
||||
from calibre.utils.bibtex import BibTeX
|
||||
|
||||
def create_bibtex_entry(entry, fields, mode, template_citation,
|
||||
bibtexdict, citation_bibtex=True, calibre_files=True):
|
||||
|
||||
@ -365,6 +363,11 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
#\n removal
|
||||
item = item.replace(u'\r\n',u' ')
|
||||
item = item.replace(u'\n',u' ')
|
||||
#html to text
|
||||
try:
|
||||
item = html2text(item)
|
||||
except:
|
||||
log.warn("Failed to convert comments to text")
|
||||
bibtex_entry.append(u'note = "%s"' % bibtexdict.utf8ToBibtex(item))
|
||||
|
||||
elif field == 'isbn' :
|
||||
@ -941,6 +944,7 @@ class EPUB_MOBI(CatalogPlugin):
|
||||
catalog.createDirectoryStructure()
|
||||
catalog.copyResources()
|
||||
catalog.buildSources()
|
||||
Options managed in gui2.catalog.catalog_epub_mobi.py
|
||||
'''
|
||||
|
||||
# A single number creates 'Last x days' only.
|
||||
|
@ -33,7 +33,7 @@ from calibre import isbytestring
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
||||
from calibre.utils.config import prefs, tweaks, from_json, to_json
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.icu import sort_key, strcmp
|
||||
from calibre.utils.search_query_parser import saved_searches, set_saved_searches
|
||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||
from calibre.utils.magick.draw import save_cover_data_to
|
||||
@ -1920,6 +1920,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
result.append(r)
|
||||
return ' & '.join(result).replace('|', ',')
|
||||
|
||||
def _update_author_in_cache(self, id_, ss, final_authors):
|
||||
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id_))
|
||||
self.data.set(id_, self.FIELD_MAP['authors'],
|
||||
','.join([a.replace(',', '|') for a in final_authors]),
|
||||
row_is_id=True)
|
||||
self.data.set(id_, self.FIELD_MAP['author_sort'], ss, row_is_id=True)
|
||||
|
||||
aum = self.authors_with_sort_strings(id_, index_is_id=True)
|
||||
self.data.set(id_, self.FIELD_MAP['au_map'],
|
||||
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
|
||||
row_is_id=True)
|
||||
|
||||
def _set_authors(self, id, authors, allow_case_change=False):
|
||||
if not authors:
|
||||
authors = [_('Unknown')]
|
||||
@ -1933,14 +1945,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
a = a.strip().replace(',', '|')
|
||||
if not isinstance(a, unicode):
|
||||
a = a.decode(preferred_encoding, 'replace')
|
||||
aus = self.conn.get('SELECT id, name FROM authors WHERE name=?', (a,))
|
||||
aus = self.conn.get('SELECT id, name, sort FROM authors WHERE name=?', (a,))
|
||||
if aus:
|
||||
aid, name = aus[0]
|
||||
aid, name, sort = aus[0]
|
||||
# Handle change of case
|
||||
if name != a:
|
||||
if allow_case_change:
|
||||
self.conn.execute('''UPDATE authors
|
||||
SET name=? WHERE id=?''', (a, aid))
|
||||
ns = author_to_author_sort(a.replace('|', ','))
|
||||
if strcmp(sort, ns) == 0:
|
||||
sort = ns
|
||||
self.conn.execute('''UPDATE authors SET name=?, sort=?
|
||||
WHERE id=?''', (a, sort, aid))
|
||||
case_change = True
|
||||
else:
|
||||
a = name
|
||||
@ -1957,17 +1972,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
bks = self.conn.get('''SELECT book FROM books_authors_link
|
||||
WHERE author=?''', (aid,))
|
||||
books_to_refresh |= set([bk[0] for bk in bks])
|
||||
for bk in books_to_refresh:
|
||||
ss = self.author_sort_from_book(id, index_is_id=True)
|
||||
aus = self.author_sort(bk, index_is_id=True)
|
||||
if strcmp(aus, ss) == 0:
|
||||
self._update_author_in_cache(bk, ss, final_authors)
|
||||
# This can repeat what was done above in rare cases. Let it.
|
||||
ss = self.author_sort_from_book(id, index_is_id=True)
|
||||
self.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)
|
||||
self._update_author_in_cache(id, ss, final_authors)
|
||||
return books_to_refresh
|
||||
|
||||
def set_authors(self, id, authors, notify=True, commit=True,
|
||||
@ -2273,6 +2285,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return []
|
||||
return result
|
||||
|
||||
def get_author_id(self, author):
|
||||
author = author.replace(',', '|')
|
||||
result = self.conn.get('SELECT id FROM authors WHERE name=?',
|
||||
(author,), all=False)
|
||||
return result
|
||||
|
||||
def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
|
||||
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
|
||||
(new_sort.strip(), old_id))
|
||||
|
@ -100,7 +100,9 @@ Device Integration
|
||||
|
||||
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|?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -133,6 +135,11 @@ Follow these steps to find the problem:
|
||||
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
|
||||
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker <http://bugs.calibre-ebook.com>`_.
|
||||
|
||||
My device is non-standard or unusual. What can I do to connect to it?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
|
||||
|
||||
How does |app| manage collections on my SONY reader?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -468,6 +475,18 @@ If it still wont launch, start a command prompt (press the windows key and R; th
|
||||
|
||||
Post any output you see in a help message on the `Forum <http://www.mobileread.com/forums/forumdisplay.php?f=166>`_.
|
||||
|
||||
|app| freezes when I click on anything?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are three possible things I know of, that can cause this:
|
||||
|
||||
* You recently connected an external monitor or TV to your computer. In this case, whenever |app| opens a new window like the edit metadata window or the conversion dialog, it appears on the second monitor where you dont notice it and so you think |app| has frozen. Disconnect your second monitor and restart calibre.
|
||||
|
||||
* You are using a Wacom branded mouse. There is an incompatibility between Wacom mice and the graphics toolkit |app| uses. Try using a non-Wacom mouse.
|
||||
|
||||
* You have invalid files in your fonts folder. If this is the case, start |app| in debug mode as desribed in the previous answer and you will get messages about invalid files in :file:`C:\\Windows\\fonts`. Delete these files and you will be fine.
|
||||
|
||||
|
||||
|app| is not starting on OS X?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -545,7 +564,7 @@ You have two choices:
|
||||
|
||||
How is |app| licensed?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode <http://code.google.com/p/calibre-ebook/downloads/list>`_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without maing your software open source. For details, see `The GNU GPL v3 <http://www.gnu.org/licenses/gpl.html>`_.
|
||||
|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode <http://code.google.com/p/calibre-ebook/downloads/list>`_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without making your software open source. For details, see `The GNU GPL v3 <http://www.gnu.org/licenses/gpl.html>`_.
|
||||
|
||||
How do I run calibre from my USB stick?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -19,7 +19,7 @@ Editing the metadata of one book at a time
|
||||
Click the book you want to edit and then click the :guilabel:`Edit metadata` button or press the ``E`` key. A dialog opens that allows you to edit all aspects of the metadata. It has various features to make editing faster and more efficient. A list of the commonly used tips:
|
||||
|
||||
* You can click the button in between title and authors to swap them automatically.
|
||||
* You can click the button next to author sort to automatically to have |app| automatically fill it from the author name.
|
||||
* You can click the button next to author sort to have |app| automatically fill it in using the sort values stored with each author. Use the :guilabel:`Manage authors` dialog to see and change the authors' sort values. This dialog can be opened by clicking and holding the button next to author sort.
|
||||
* You can click the button next to tags to use the Tag Editor to manage the tags associated with the book.
|
||||
* The ISBN box will have a red background if you enter an invalid ISBN. It will be green for valid ISBNs
|
||||
* The author sort box will be red if the author sort value differs from what |app| thinks it should be.
|
||||
|
File diff suppressed because it is too large
Load Diff
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