mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Merge from trunk
This commit is contained in:
commit
c9cb3eba48
@ -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'
|
||||||
|
@ -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 = (
|
||||||
@ -637,7 +635,6 @@ 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:
|
||||||
@ -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':
|
||||||
@ -724,7 +721,10 @@ class Amazon(Source):
|
|||||||
|
|
||||||
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,7 +751,6 @@ 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]
|
||||||
@ -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)
|
||||||
|
|
||||||
@ -907,6 +904,11 @@ if __name__ == '__main__': # tests {{{
|
|||||||
isbn_test, title_test, authors_test, comments_test, series_test)
|
isbn_test, title_test, authors_test, comments_test, series_test)
|
||||||
com_tests = [ # {{{
|
com_tests = [ # {{{
|
||||||
|
|
||||||
|
( # 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
|
( # + 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',
|
||||||
@ -917,8 +919,8 @@ if __name__ == '__main__': # tests {{{
|
|||||||
( # 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'),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@ -926,8 +928,8 @@ if __name__ == '__main__': # tests {{{
|
|||||||
( # 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'),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@ -1016,7 +1018,7 @@ if __name__ == '__main__': # tests {{{
|
|||||||
),
|
),
|
||||||
|
|
||||||
( # 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'上橋 菜穂子'])
|
||||||
]
|
]
|
||||||
|
@ -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,39 +469,33 @@ 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)
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
else:
|
else:
|
||||||
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
|
dest_value = unicode(dest_value) + u'\n\n' + unicode(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 \
|
if (dt in {'bool', 'int', 'float', 'rating', 'datetime'} and dest_value is None):
|
||||||
('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)
|
||||||
if db.field_metadata[key]['datatype'] == 'series' \
|
if (dt == 'series' and not dest_value and src_value):
|
||||||
and not dest_value:
|
|
||||||
if src_value:
|
|
||||||
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
|
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)
|
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
|
||||||
if (db.field_metadata[key]['datatype'] == 'enumeration' or
|
if (dt == 'enumeration' or (dt == 'text' and not fm['is_multiple']) and not dest_value):
|
||||||
(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)
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
if db.field_metadata[key]['datatype'] == 'text' \
|
if (dt == 'text' and fm['is_multiple'] and src_value):
|
||||||
and db.field_metadata[key]['is_multiple']:
|
|
||||||
if src_value:
|
|
||||||
if not dest_value:
|
if not dest_value:
|
||||||
dest_value = src_value
|
dest_value = src_value
|
||||||
else:
|
else:
|
||||||
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user