Merge from trunk

This commit is contained in:
Charles Haley 2013-04-10 08:46:08 +02:00
commit c9cb3eba48
4 changed files with 122 additions and 102 deletions

View File

@ -17,6 +17,7 @@ from calibre.db.tests.base import BaseTest
class WritingTest(BaseTest): class WritingTest(BaseTest):
# Utils {{{
def create_getter(self, name, getter=None): def create_getter(self, name, getter=None):
if getter is None: if getter is None:
if name.endswith('_index'): if name.endswith('_index'):
@ -71,6 +72,7 @@ class WritingTest(BaseTest):
'Failed setting for %s, sqlite value not the same: %r != %r'%( 'Failed setting for %s, sqlite value not the same: %r != %r'%(
test.name, old_sqlite_res, sqlite_res)) test.name, old_sqlite_res, sqlite_res))
del db del db
# }}}
def test_one_one(self): # {{{ def test_one_one(self): # {{{
'Test setting of values in one-one fields' 'Test setting of values in one-one fields'

View File

@ -21,7 +21,7 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_only_date from calibre.utils.date import parse_only_date
from calibre.utils.localization import canonicalize_lang from calibre.utils.localization import canonicalize_lang
class Worker(Thread): # Get details {{{ class Worker(Thread): # Get details {{{
''' '''
Get book details from amazons book page in a separate thread Get book details from amazons book page in a separate thread
@ -43,12 +43,12 @@ class Worker(Thread): # Get details {{{
months = { months = {
'de': { 'de': {
1 : ['jän'], 1: ['jän'],
2 : ['februar'], 2: ['februar'],
3 : ['märz'], 3: ['märz'],
5 : ['mai'], 5: ['mai'],
6 : ['juni'], 6: ['juni'],
7 : ['juli'], 7: ['juli'],
10: ['okt'], 10: ['okt'],
12: ['dez'] 12: ['dez']
}, },
@ -276,7 +276,6 @@ class Worker(Thread): # Get details {{{
self.log.exception('Error parsing authors for url: %r'%self.url) self.log.exception('Error parsing authors for url: %r'%self.url)
authors = [] authors = []
if not title or not authors or not asin: if not title or not authors or not asin:
self.log.error('Could not find title/authors/asin for %r'%self.url) self.log.error('Could not find title/authors/asin for %r'%self.url)
self.log.error('ASIN: %r Title: %r Authors: %r'%(asin, title, self.log.error('ASIN: %r Title: %r Authors: %r'%(asin, title,
@ -431,7 +430,6 @@ class Worker(Thread): # Get details {{{
desc = re.sub(r'(?s)<!--.*?-->', '', desc) desc = re.sub(r'(?s)<!--.*?-->', '', desc)
return sanitize_comments_html(desc) return sanitize_comments_html(desc)
def parse_comments(self, root): def parse_comments(self, root):
ans = '' ans = ''
desc = root.xpath('//div[@id="ps-content"]/div[@class="content"]') desc = root.xpath('//div[@id="ps-content"]/div[@class="content"]')
@ -528,13 +526,13 @@ class Amazon(Source):
AMAZON_DOMAINS = { AMAZON_DOMAINS = {
'com': _('US'), 'com': _('US'),
'fr' : _('France'), 'fr': _('France'),
'de' : _('Germany'), 'de': _('Germany'),
'uk' : _('UK'), 'uk': _('UK'),
'it' : _('Italy'), 'it': _('Italy'),
'jp' : _('Japan'), 'jp': _('Japan'),
'es' : _('Spain'), 'es': _('Spain'),
'br' : _('Brazil'), 'br': _('Brazil'),
} }
options = ( options = (
@ -592,7 +590,7 @@ class Amazon(Source):
return domain, val return domain, val
return None, None return None, None
def get_book_url(self, identifiers): # {{{ def get_book_url(self, identifiers): # {{{
domain, asin = self.get_domain_and_asin(identifiers) domain, asin = self.get_domain_and_asin(identifiers)
if domain and asin: if domain and asin:
url = None url = None
@ -637,8 +635,7 @@ class Amazon(Source):
mi.tags = list(map(fixcase, mi.tags)) mi.tags = list(map(fixcase, mi.tags))
mi.isbn = check_isbn(mi.isbn) mi.isbn = check_isbn(mi.isbn)
def create_query(self, log, title=None, authors=None, identifiers={}, # {{{
def create_query(self, log, title=None, authors=None, identifiers={}, # {{{
domain=None): domain=None):
if domain is None: if domain is None:
domain = self.domain domain = self.domain
@ -648,8 +645,8 @@ class Amazon(Source):
domain = idomain domain = idomain
# See the amazon detailed search page to get all options # See the amazon detailed search page to get all options
q = { 'search-alias' : 'aps', q = {'search-alias': 'aps',
'unfiltered' : '1', 'unfiltered': '1',
} }
if domain == 'com': if domain == 'com':
@ -704,7 +701,7 @@ class Amazon(Source):
# }}} # }}}
def get_cached_cover_url(self, identifiers): # {{{ def get_cached_cover_url(self, identifiers): # {{{
url = None url = None
domain, asin = self.get_domain_and_asin(identifiers) domain, asin = self.get_domain_and_asin(identifiers)
if asin is None: if asin is None:
@ -717,14 +714,17 @@ class Amazon(Source):
return url return url
# }}} # }}}
def parse_results_page(self, root): # {{{ def parse_results_page(self, root): # {{{
from lxml.html import tostring from lxml.html import tostring
matches = [] matches = []
def title_ok(title): def title_ok(title):
title = title.lower() title = title.lower()
for x in ('bulk pack', '[audiobook]', '[audio cd]'): bad = ['bulk pack', '[audiobook]', '[audio cd]']
if self.domain == 'com':
bad.append('(spanish edition)')
for x in bad:
if x in title: if x in title:
return False return False
return True return True
@ -751,13 +751,12 @@ class Amazon(Source):
matches.append(a.get('href')) matches.append(a.get('href'))
break break
# Keep only the top 5 matches as the matches are sorted by relevance by # Keep only the top 5 matches as the matches are sorted by relevance by
# Amazon so lower matches are not likely to be very relevant # Amazon so lower matches are not likely to be very relevant
return matches[:5] return matches[:5]
# }}} # }}}
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
identifiers={}, timeout=30): identifiers={}, timeout=30):
''' '''
Note this method will retry without identifiers automatically if no Note this method will retry without identifiers automatically if no
@ -795,7 +794,6 @@ class Amazon(Source):
log.exception(msg) log.exception(msg)
return as_unicode(msg) return as_unicode(msg)
raw = clean_ascii_chars(xml_to_unicode(raw, raw = clean_ascii_chars(xml_to_unicode(raw,
strip_encoding_pats=True, resolve_entities=True)[0]) strip_encoding_pats=True, resolve_entities=True)[0])
@ -825,7 +823,6 @@ class Amazon(Source):
# The error is almost always a not found error # The error is almost always a not found error
found = False found = False
if found: if found:
matches = self.parse_results_page(root) matches = self.parse_results_page(root)
@ -863,7 +860,7 @@ class Amazon(Source):
return None return None
# }}} # }}}
def download_cover(self, log, result_queue, abort, # {{{ def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
cached_url = self.get_cached_cover_url(identifiers) cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None: if cached_url is None:
@ -900,39 +897,44 @@ class Amazon(Source):
log.exception('Failed to download cover from:', cached_url) log.exception('Failed to download cover from:', cached_url)
# }}} # }}}
if __name__ == '__main__': # tests {{{ if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e # To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/amazon.py # src/calibre/ebooks/metadata/sources/amazon.py
from calibre.ebooks.metadata.sources.test import (test_identify_plugin, from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
isbn_test, title_test, authors_test, comments_test, series_test) isbn_test, title_test, authors_test, comments_test, series_test)
com_tests = [ # {{{ com_tests = [ # {{{
( # + in title and uses id="main-image" for cover ( # Has a spanish edition
{'title':'11/22/63'},
[title_test('11/22/63: A Novel', exact=True), authors_test(['Stephen King']),]
),
( # + in title and uses id="main-image" for cover
{'title':'C++ Concurrency in Action'}, {'title':'C++ Concurrency in Action'},
[title_test('C++ Concurrency in Action: Practical Multithreading', [title_test('C++ Concurrency in Action: Practical Multithreading',
exact=True), exact=True),
] ]
), ),
( # Series ( # Series
{'identifiers':{'amazon':'0756407117'}}, {'identifiers':{'amazon':'0756407117'}},
[title_test( [title_test(
"Throne of the Crescent Moon" "Throne of the Crescent Moon",
, exact=True), series_test('Crescent Moon Kingdoms', 1), exact=True), series_test('Crescent Moon Kingdoms', 1),
comments_test('Makhslood'), comments_test('Makhslood'),
] ]
), ),
( # Different comments markup, using Book Description section ( # Different comments markup, using Book Description section
{'identifiers':{'amazon':'0982514506'}}, {'identifiers':{'amazon':'0982514506'}},
[title_test( [title_test(
"Griffin's Destiny: Book Three: The Griffin's Daughter Trilogy" "Griffin's Destiny: Book Three: The Griffin's Daughter Trilogy",
, exact=True), exact=True),
comments_test('Jelena'), comments_test('Leslie'), comments_test('Jelena'), comments_test('Leslie'),
] ]
), ),
( # # in title ( # # in title
{'title':'Expert C# 2008 Business Objects', {'title':'Expert C# 2008 Business Objects',
'authors':['Lhotka']}, 'authors':['Lhotka']},
[title_test('Expert C# 2008 Business Objects', exact=True), [title_test('Expert C# 2008 Business Objects', exact=True),
@ -948,13 +950,13 @@ if __name__ == '__main__': # tests {{{
), ),
( # Sophisticated comment formatting ( # Sophisticated comment formatting
{'identifiers':{'isbn': '9781416580829'}}, {'identifiers':{'isbn': '9781416580829'}},
[title_test('Angels & Demons - Movie Tie-In: A Novel', [title_test('Angels & Demons - Movie Tie-In: A Novel',
exact=True), authors_test(['Dan Brown'])] exact=True), authors_test(['Dan Brown'])]
), ),
( # No specific problems ( # No specific problems
{'identifiers':{'isbn': '0743273567'}}, {'identifiers':{'isbn': '0743273567'}},
[title_test('The great gatsby', exact=True), [title_test('The great gatsby', exact=True),
authors_test(['F. Scott Fitzgerald'])] authors_test(['F. Scott Fitzgerald'])]
@ -967,9 +969,9 @@ if __name__ == '__main__': # tests {{{
), ),
] # }}} ] # }}}
de_tests = [ # {{{ de_tests = [ # {{{
( (
{'identifiers':{'isbn': '3548283519'}}, {'identifiers':{'isbn': '3548283519'}},
[title_test('Wer Wind Sät: Der Fünfte Fall Für Bodenstein Und Kirchhoff', [title_test('Wer Wind Sät: Der Fünfte Fall Für Bodenstein Und Kirchhoff',
@ -977,9 +979,9 @@ if __name__ == '__main__': # tests {{{
] ]
), ),
] # }}} ] # }}}
it_tests = [ # {{{ it_tests = [ # {{{
( (
{'identifiers':{'isbn': '8838922195'}}, {'identifiers':{'isbn': '8838922195'}},
[title_test('La briscola in cinque', [title_test('La briscola in cinque',
@ -987,9 +989,9 @@ if __name__ == '__main__': # tests {{{
] ]
), ),
] # }}} ] # }}}
fr_tests = [ # {{{ fr_tests = [ # {{{
( (
{'identifiers':{'isbn': '2221116798'}}, {'identifiers':{'isbn': '2221116798'}},
[title_test('L\'étrange voyage de Monsieur Daldry', [title_test('L\'étrange voyage de Monsieur Daldry',
@ -997,9 +999,9 @@ if __name__ == '__main__': # tests {{{
] ]
), ),
] # }}} ] # }}}
es_tests = [ # {{{ es_tests = [ # {{{
( (
{'identifiers':{'isbn': '8483460831'}}, {'identifiers':{'isbn': '8483460831'}},
[title_test('Tiempos Interesantes', [title_test('Tiempos Interesantes',
@ -1007,28 +1009,28 @@ if __name__ == '__main__': # tests {{{
] ]
), ),
] # }}} ] # }}}
jp_tests = [ # {{{ jp_tests = [ # {{{
( # Adult filtering test ( # Adult filtering test
{'identifiers':{'isbn':'4799500066'}}, {'identifiers':{'isbn':'4799500066'}},
[title_test(u' '),] [title_test(u' '),]
), ),
( # isbn -> title, authors ( # isbn -> title, authors
{'identifiers':{'isbn': '9784101302720' }}, {'identifiers':{'isbn': '9784101302720'}},
[title_test(u'精霊の守り人', [title_test(u'精霊の守り人',
exact=True), authors_test([u'上橋 菜穂子']) exact=True), authors_test([u'上橋 菜穂子'])
] ]
), ),
( # title, authors -> isbn (will use Shift_JIS encoding in query.) ( # title, authors -> isbn (will use Shift_JIS encoding in query.)
{'title': u'考えない練習', {'title': u'考えない練習',
'authors': [u'小池 龍之介']}, 'authors': [u'小池 龍之介']},
[isbn_test('9784093881067'), ] [isbn_test('9784093881067'), ]
), ),
] # }}} ] # }}}
br_tests = [ # {{{ br_tests = [ # {{{
( (
{'title':'Guerra dos Tronos'}, {'title':'Guerra dos Tronos'},
[title_test('A Guerra dos Tronos - As Crônicas de Gelo e Fogo', [title_test('A Guerra dos Tronos - As Crônicas de Gelo e Fogo',
@ -1036,7 +1038,7 @@ if __name__ == '__main__': # tests {{{
] ]
), ),
] # }}} ] # }}}
def do_test(domain, start=0, stop=None): def do_test(domain, start=0, stop=None):
tests = globals().get(domain+'_tests') tests = globals().get(domain+'_tests')

View File

@ -279,7 +279,7 @@ class EditMetadataAction(InterfaceAction):
''' '''
Edit metadata of selected books in library in bulk. Edit metadata of selected books in library in bulk.
''' '''
rows = [r.row() for r in \ rows = [r.row() for r in
self.gui.library_view.selectionModel().selectedRows()] self.gui.library_view.selectionModel().selectedRows()]
m = self.gui.library_view.model() m = self.gui.library_view.model()
ids = [m.id(r) for r in rows] ids = [m.id(r) for r in rows]
@ -469,45 +469,39 @@ class EditMetadataAction(InterfaceAction):
if not had_orig_cover and dest_cover: if not had_orig_cover and dest_cover:
db.set_cover(dest_id, dest_cover) db.set_cover(dest_id, dest_cover)
for key in db.field_metadata: #loop thru all defined fields for key in db.field_metadata: # loop thru all defined fields
if db.field_metadata[key]['is_custom']: fm = db.field_metadata[key]
colnum = db.field_metadata[key]['colnum'] if not fm['is_custom']:
continue
dt = fm['datatype']
colnum = fm['colnum']
# Get orig_dest_comments before it gets changed # Get orig_dest_comments before it gets changed
if db.field_metadata[key]['datatype'] == 'comments': if dt == 'comments':
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True) orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
for src_id in src_ids: for src_id in src_ids:
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True) dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
src_value = db.get_custom(src_id, num=colnum, index_is_id=True) src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
if db.field_metadata[key]['datatype'] == 'comments': if (dt == 'comments' and src_value and src_value != orig_dest_value):
if src_value and src_value != orig_dest_value: if not dest_value:
if not dest_value: db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if (dt in {'bool', 'int', 'float', 'rating', 'datetime'} and dest_value is None):
db.set_custom(dest_id, src_value, num=colnum) db.set_custom(dest_id, src_value, num=colnum)
else: if (dt == 'series' and not dest_value and src_value):
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value) src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
if (dt == 'enumeration' or (dt == 'text' and not fm['is_multiple']) and not dest_value):
db.set_custom(dest_id, src_value, num=colnum)
if (dt == 'text' and fm['is_multiple'] and src_value):
if not dest_value:
dest_value = src_value
else:
dest_value.extend(src_value)
db.set_custom(dest_id, dest_value, num=colnum) db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \ # }}}
('bool', 'int', 'float', 'rating', 'datetime') \
and dest_value is None:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'series' \
and not dest_value:
if src_value:
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
if (db.field_metadata[key]['datatype'] == 'enumeration' or
(db.field_metadata[key]['datatype'] == 'text' and
not db.field_metadata[key]['is_multiple'])
and not dest_value):
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'text' \
and db.field_metadata[key]['is_multiple']:
if src_value:
if not dest_value:
dest_value = src_value
else:
dest_value.extend(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
# }}}
def edit_device_collections(self, view, oncard=None): def edit_device_collections(self, view, oncard=None):
model = view.model() model = view.model()
@ -515,8 +509,8 @@ class EditMetadataAction(InterfaceAction):
d = DeviceCategoryEditor(self.gui, tag_to_match=None, data=result, key=sort_key) d = DeviceCategoryEditor(self.gui, tag_to_match=None, data=result, key=sort_key)
d.exec_() d.exec_()
if d.result() == d.Accepted: if d.result() == d.Accepted:
to_rename = d.to_rename # dict of new text to old ids to_rename = d.to_rename # dict of new text to old ids
to_delete = d.to_delete # list of ids to_delete = d.to_delete # list of ids
for old_id, new_name in to_rename.iteritems(): for old_id, new_name in to_rename.iteritems():
model.rename_collection(old_id, new_name=unicode(new_name)) model.rename_collection(old_id, new_name=unicode(new_name))
for item in to_delete: for item in to_delete:
@ -585,7 +579,6 @@ class EditMetadataAction(InterfaceAction):
self.apply_pd.value += 1 self.apply_pd.value += 1
QTimer.singleShot(50, self.do_one_apply) QTimer.singleShot(50, self.do_one_apply)
def apply_mi(self, book_id, mi): def apply_mi(self, book_id, mi):
db = self.gui.current_db db = self.gui.current_db

View File

@ -18,7 +18,8 @@ from calibre.gui2.dialogs.message_box import ViewLog
Question = namedtuple('Question', 'payload callback cancel_callback ' Question = namedtuple('Question', 'payload callback cancel_callback '
'title msg html_log log_viewer_title log_is_file det_msg ' 'title msg html_log log_viewer_title log_is_file det_msg '
'show_copy_button checkbox_msg checkbox_checked') 'show_copy_button checkbox_msg checkbox_checked action_callback '
'action_label action_icon')
class ProceedQuestion(QDialog): class ProceedQuestion(QDialog):
@ -51,6 +52,8 @@ class ProceedQuestion(QDialog):
self.copy_button = self.bb.addButton(_('&Copy to clipboard'), self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
self.bb.ActionRole) self.bb.ActionRole)
self.copy_button.clicked.connect(self.copy_to_clipboard) self.copy_button.clicked.connect(self.copy_to_clipboard)
self.action_button = self.bb.addButton('', self.bb.ActionRole)
self.action_button.clicked.connect(self.action_clicked)
self.show_det_msg = _('Show &details') self.show_det_msg = _('Show &details')
self.hide_det_msg = _('Hide &details') self.hide_det_msg = _('Hide &details')
self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole) self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
@ -81,6 +84,12 @@ class ProceedQuestion(QDialog):
unicode(self.det_msg.toPlainText()))) unicode(self.det_msg.toPlainText())))
self.copy_button.setText(_('Copied')) self.copy_button.setText(_('Copied'))
def action_clicked(self):
if self.questions:
q = self.questions[0]
self.questions[0] = q._replace(callback=q.action_callback)
self.accept()
def accept(self): def accept(self):
if self.questions: if self.questions:
payload, callback, cancel_callback = self.questions[0][:3] payload, callback, cancel_callback = self.questions[0][:3]
@ -131,6 +140,11 @@ class ProceedQuestion(QDialog):
self.setWindowTitle(question.title) self.setWindowTitle(question.title)
self.log_button.setVisible(bool(question.html_log)) self.log_button.setVisible(bool(question.html_log))
self.copy_button.setVisible(bool(question.show_copy_button)) self.copy_button.setVisible(bool(question.show_copy_button))
self.action_button.setVisible(question.action_callback is not None)
if question.action_callback is not None:
self.action_button.setText(question.action_label or '')
self.action_button.setIcon(
QIcon() if question.action_icon is None else question.action_icon)
self.det_msg.setPlainText(question.det_msg or '') self.det_msg.setPlainText(question.det_msg or '')
self.det_msg.setVisible(False) self.det_msg.setVisible(False)
self.det_msg_toggle.setVisible(bool(question.det_msg)) self.det_msg_toggle.setVisible(bool(question.det_msg))
@ -146,7 +160,8 @@ class ProceedQuestion(QDialog):
def __call__(self, callback, payload, html_log, log_viewer_title, title, def __call__(self, callback, payload, html_log, log_viewer_title, title,
msg, det_msg='', show_copy_button=False, cancel_callback=None, msg, det_msg='', show_copy_button=False, cancel_callback=None,
log_is_file=False, checkbox_msg=None, checkbox_checked=False): log_is_file=False, checkbox_msg=None, checkbox_checked=False,
action_callback=None, action_label=None, action_icon=None):
''' '''
A non modal popup that notifies the user that a background task has A non modal popup that notifies the user that a background task has
been completed. This class guarantees that only a single popup is been completed. This class guarantees that only a single popup is
@ -171,11 +186,19 @@ class ProceedQuestion(QDialog):
called with both the payload and the state of the called with both the payload and the state of the
checkbox as arguments. checkbox as arguments.
:param checkbox_checked: If True the checkbox is checked by default. :param checkbox_checked: If True the checkbox is checked by default.
:param action_callback: If not None, an extra button is added, which
when clicked will cause action_callback to be called
instead of callback. action_callback is called in
exactly the same way as callback.
:param action_label: The text on the action button
:param action_icon: The icon for the action button, must be a QIcon object or None
''' '''
question = Question(payload, callback, cancel_callback, title, msg, question = Question(
html_log, log_viewer_title, log_is_file, det_msg, payload, callback, cancel_callback, title, msg, html_log,
show_copy_button, checkbox_msg, checkbox_checked) log_viewer_title, log_is_file, det_msg, show_copy_button,
checkbox_msg, checkbox_checked, action_callback, action_label,
action_icon)
self.questions.append(question) self.questions.append(question)
self.show_question() self.show_question()