From f45ca90c271603b945dd2d03ba8ca2d802ca2125 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 9 Apr 2013 16:25:37 +0530 Subject: [PATCH 1/4] Allow adding an action button to the process dialog --- src/calibre/gui2/proceed.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/proceed.py b/src/calibre/gui2/proceed.py index d09c247bd0..67efe48b53 100644 --- a/src/calibre/gui2/proceed.py +++ b/src/calibre/gui2/proceed.py @@ -18,7 +18,8 @@ from calibre.gui2.dialogs.message_box import ViewLog Question = namedtuple('Question', 'payload callback cancel_callback ' '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): @@ -51,6 +52,8 @@ class ProceedQuestion(QDialog): self.copy_button = self.bb.addButton(_('&Copy to clipboard'), self.bb.ActionRole) 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.hide_det_msg = _('Hide &details') 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()))) 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): if self.questions: payload, callback, cancel_callback = self.questions[0][:3] @@ -131,6 +140,11 @@ class ProceedQuestion(QDialog): self.setWindowTitle(question.title) self.log_button.setVisible(bool(question.html_log)) 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.setVisible(False) 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, 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 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 checkbox as arguments. :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, - html_log, log_viewer_title, log_is_file, det_msg, - show_copy_button, checkbox_msg, checkbox_checked) + question = Question( + payload, callback, cancel_callback, title, msg, html_log, + 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.show_question() From 9fb122cd4b35cff9d4da2130664270805762cc3a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 9 Apr 2013 16:44:46 +0530 Subject: [PATCH 2/4] cleanup --- src/calibre/gui2/actions/edit_metadata.py | 71 ++++++++++------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 4a78c6663a..485bc5bf90 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -279,7 +279,7 @@ class EditMetadataAction(InterfaceAction): ''' 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()] m = self.gui.library_view.model() ids = [m.id(r) for r in rows] @@ -469,45 +469,39 @@ class EditMetadataAction(InterfaceAction): if not had_orig_cover and dest_cover: db.set_cover(dest_id, dest_cover) - for key in db.field_metadata: #loop thru all defined fields - if db.field_metadata[key]['is_custom']: - colnum = db.field_metadata[key]['colnum'] + for key in db.field_metadata: # loop thru all defined fields + fm = db.field_metadata[key] + if not fm['is_custom']: + continue + dt = fm['datatype'] + colnum = fm['colnum'] # Get orig_dest_comments before it gets changed - if db.field_metadata[key]['datatype'] == 'comments': - orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True) + if dt == 'comments': + orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True) + for src_id in src_ids: - 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) - if db.field_metadata[key]['datatype'] == 'comments': - if src_value and src_value != orig_dest_value: - if not dest_value: + 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) + if (dt == 'comments' and src_value and src_value != orig_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) - else: - dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value) + if (dt == 'series' and not dest_value and 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) - 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): model = view.model() @@ -515,8 +509,8 @@ class EditMetadataAction(InterfaceAction): d = DeviceCategoryEditor(self.gui, tag_to_match=None, data=result, key=sort_key) d.exec_() if d.result() == d.Accepted: - to_rename = d.to_rename # dict of new text to old ids - to_delete = d.to_delete # list of ids + to_rename = d.to_rename # dict of new text to old ids + to_delete = d.to_delete # list of ids for old_id, new_name in to_rename.iteritems(): model.rename_collection(old_id, new_name=unicode(new_name)) for item in to_delete: @@ -585,7 +579,6 @@ class EditMetadataAction(InterfaceAction): self.apply_pd.value += 1 QTimer.singleShot(50, self.do_one_apply) - def apply_mi(self, book_id, mi): db = self.gui.current_db From 949b3c04f9ac927456f71e8f9df808010e831b35 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 9 Apr 2013 17:07:24 +0530 Subject: [PATCH 3/4] Amazon metadata download: Ignore Spanish edition entries when searching for a book on amazon.com --- src/calibre/ebooks/metadata/sources/amazon.py | 118 +++++++++--------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index fe39c3cd16..4509608135 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -21,7 +21,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.date import parse_only_date 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 @@ -43,12 +43,12 @@ class Worker(Thread): # Get details {{{ months = { 'de': { - 1 : ['jän'], - 2 : ['februar'], - 3 : ['märz'], - 5 : ['mai'], - 6 : ['juni'], - 7 : ['juli'], + 1: ['jän'], + 2: ['februar'], + 3: ['märz'], + 5: ['mai'], + 6: ['juni'], + 7: ['juli'], 10: ['okt'], 12: ['dez'] }, @@ -276,7 +276,6 @@ class Worker(Thread): # Get details {{{ self.log.exception('Error parsing authors for url: %r'%self.url) authors = [] - 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('ASIN: %r Title: %r Authors: %r'%(asin, title, @@ -431,7 +430,6 @@ class Worker(Thread): # Get details {{{ desc = re.sub(r'(?s)', '', desc) return sanitize_comments_html(desc) - def parse_comments(self, root): ans = '' desc = root.xpath('//div[@id="ps-content"]/div[@class="content"]') @@ -528,13 +526,13 @@ class Amazon(Source): AMAZON_DOMAINS = { 'com': _('US'), - 'fr' : _('France'), - 'de' : _('Germany'), - 'uk' : _('UK'), - 'it' : _('Italy'), - 'jp' : _('Japan'), - 'es' : _('Spain'), - 'br' : _('Brazil'), + 'fr': _('France'), + 'de': _('Germany'), + 'uk': _('UK'), + 'it': _('Italy'), + 'jp': _('Japan'), + 'es': _('Spain'), + 'br': _('Brazil'), } options = ( @@ -592,7 +590,7 @@ class Amazon(Source): return domain, val return None, None - def get_book_url(self, identifiers): # {{{ + def get_book_url(self, identifiers): # {{{ domain, asin = self.get_domain_and_asin(identifiers) if domain and asin: url = None @@ -637,8 +635,7 @@ class Amazon(Source): mi.tags = list(map(fixcase, mi.tags)) 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): if domain is None: domain = self.domain @@ -648,8 +645,8 @@ class Amazon(Source): domain = idomain # See the amazon detailed search page to get all options - q = { 'search-alias' : 'aps', - 'unfiltered' : '1', + q = {'search-alias': 'aps', + 'unfiltered': '1', } 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 domain, asin = self.get_domain_and_asin(identifiers) if asin is None: @@ -717,14 +714,17 @@ class Amazon(Source): return url # }}} - def parse_results_page(self, root): # {{{ + def parse_results_page(self, root): # {{{ from lxml.html import tostring matches = [] def title_ok(title): 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: return False return True @@ -751,13 +751,12 @@ class Amazon(Source): matches.append(a.get('href')) break - # 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 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): ''' Note this method will retry without identifiers automatically if no @@ -795,7 +794,6 @@ class Amazon(Source): log.exception(msg) return as_unicode(msg) - raw = clean_ascii_chars(xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0]) @@ -825,7 +823,6 @@ class Amazon(Source): # The error is almost always a not found error found = False - if found: matches = self.parse_results_page(root) @@ -863,7 +860,7 @@ class Amazon(Source): 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): cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: @@ -900,39 +897,44 @@ class Amazon(Source): 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 # src/calibre/ebooks/metadata/sources/amazon.py from calibre.ebooks.metadata.sources.test import (test_identify_plugin, 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_test('C++ Concurrency in Action: Practical Multithreading', exact=True), ] ), - ( # Series + ( # Series {'identifiers':{'amazon':'0756407117'}}, [title_test( - "Throne of the Crescent Moon" - , exact=True), series_test('Crescent Moon Kingdoms', 1), + "Throne of the Crescent Moon", + exact=True), series_test('Crescent Moon Kingdoms', 1), comments_test('Makhslood'), ] ), - ( # Different comments markup, using Book Description section + ( # Different comments markup, using Book Description section {'identifiers':{'amazon':'0982514506'}}, [title_test( - "Griffin's Destiny: Book Three: The Griffin's Daughter Trilogy" - , exact=True), + "Griffin's Destiny: Book Three: The Griffin's Daughter Trilogy", + exact=True), comments_test('Jelena'), comments_test('Leslie'), ] ), - ( # # in title + ( # # in title {'title':'Expert C# 2008 Business Objects', 'authors':['Lhotka']}, [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'}}, [title_test('Angels & Demons - Movie Tie-In: A Novel', exact=True), authors_test(['Dan Brown'])] ), - ( # No specific problems + ( # No specific problems {'identifiers':{'isbn': '0743273567'}}, [title_test('The great gatsby', exact=True), authors_test(['F. Scott Fitzgerald'])] @@ -967,9 +969,9 @@ if __name__ == '__main__': # tests {{{ ), - ] # }}} + ] # }}} - de_tests = [ # {{{ + de_tests = [ # {{{ ( {'identifiers':{'isbn': '3548283519'}}, [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'}}, [title_test('La briscola in cinque', @@ -987,9 +989,9 @@ if __name__ == '__main__': # tests {{{ ] ), - ] # }}} + ] # }}} - fr_tests = [ # {{{ + fr_tests = [ # {{{ ( {'identifiers':{'isbn': '2221116798'}}, [title_test('L\'étrange voyage de Monsieur Daldry', @@ -997,9 +999,9 @@ if __name__ == '__main__': # tests {{{ ] ), - ] # }}} + ] # }}} - es_tests = [ # {{{ + es_tests = [ # {{{ ( {'identifiers':{'isbn': '8483460831'}}, [title_test('Tiempos Interesantes', @@ -1007,28 +1009,28 @@ if __name__ == '__main__': # tests {{{ ] ), - ] # }}} + ] # }}} - jp_tests = [ # {{{ - ( # Adult filtering test + jp_tests = [ # {{{ + ( # Adult filtering test {'identifiers':{'isbn':'4799500066'}}, [title_test(u'Bitch Trap'),] ), - ( # isbn -> title, authors - {'identifiers':{'isbn': '9784101302720' }}, + ( # isbn -> title, authors + {'identifiers':{'isbn': '9784101302720'}}, [title_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'考えない練習', 'authors': [u'小池 龍之介']}, [isbn_test('9784093881067'), ] ), - ] # }}} + ] # }}} - br_tests = [ # {{{ + br_tests = [ # {{{ ( {'title':'Guerra dos Tronos'}, [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): tests = globals().get(domain+'_tests') From 892b706760771db130414c54e672f05872ceaa10 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 9 Apr 2013 17:31:48 +0530 Subject: [PATCH 4/4] ... --- src/calibre/db/tests/writing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index c37a173ae4..f4fac34a57 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -17,6 +17,7 @@ from calibre.db.tests.base import BaseTest class WritingTest(BaseTest): + # Utils {{{ def create_getter(self, name, getter=None): if getter is None: if name.endswith('_index'): @@ -71,6 +72,7 @@ class WritingTest(BaseTest): 'Failed setting for %s, sqlite value not the same: %r != %r'%( test.name, old_sqlite_res, sqlite_res)) del db + # }}} def test_one_one(self): # {{{ 'Test setting of values in one-one fields'