diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index cdad5f6ff7..f7c68f0975 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' Provides abstraction for metadata reading.writing from a variety of ebook formats. """ import os, sys, re +from contextlib import suppress from calibre import relpath, guess_type, prints, force_unicode from calibre.utils.config_base import tweaks @@ -374,46 +375,59 @@ def MetaInformation(title, authors=(_('Unknown'),)): return Metadata(title, authors, other=mi) +def check_digit_for_isbn10(isbn): + check = sum((i+1)*int(isbn[i]) for i in range(9)) % 11 + return 'X' if check == 10 else str(check) + + +def check_digit_for_isbn13(isbn): + check = 10 - sum((1 if i%2 ==0 else 3)*int(isbn[i]) for i in range(12)) % 10 + if check == 10: + check = 0 + return str(check) + + def check_isbn10(isbn): - try: - digits = tuple(map(int, isbn[:9])) - products = [(i+1)*digits[i] for i in range(9)] - check = sum(products)%11 - if (check == 10 and isbn[9] == 'X') or check == int(isbn[9]): - return isbn - except Exception: - pass - return None + with suppress(Exception): + return check_digit_for_isbn10(isbn) == isbn[9] + return False def check_isbn13(isbn): - try: - digits = tuple(map(int, isbn[:12])) - products = [(1 if i%2 ==0 else 3)*digits[i] for i in range(12)] - check = 10 - (sum(products)%10) - if check == 10: - check = 0 - if unicode_type(check) == isbn[12]: - return isbn - except Exception: - pass - return None + with suppress(Exception): + return check_digit_for_isbn13(isbn) == isbn[12] + return False def check_isbn(isbn): if not isbn: return None isbn = re.sub(r'[^0-9X]', '', isbn.upper()) + il = len(isbn) + if il not in (10, 13): + return None 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: - return check_isbn13(isbn) + if il == 10: + return isbn if check_isbn10(isbn) else None + if il == 13: + return isbn if check_isbn13(isbn) else None return None +def normalize_isbn(isbn): + if not isbn: + return isbn + ans = check_isbn(isbn) + if ans is None: + return isbn + if len(ans) == 10: + ans = '978' + ans[:9] + ans += check_digit_for_isbn13(ans) + return ans + + def check_issn(issn): if not issn: return None diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 0ea9a11f7b..e97f6f4d6a 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -14,7 +14,7 @@ from qt.core import QApplication, QDialog, QPixmap, QTimer from calibre import as_unicode, guess_type from calibre.constants import iswindows from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata import MetaInformation, normalize_isbn from calibre.gui2 import ( choose_dir, choose_files, error_dialog, gprefs, info_dialog, question_dialog, warning_dialog @@ -363,8 +363,35 @@ class AddAction(InterfaceAction): for path in temp_files: os.remove(path) - def add_isbns(self, books, add_tags=[]): - self.isbn_books = list(books) + def check_for_existing_isbns(self, books): + db = self.gui.current_db.new_api + book_id_identifiers = db.all_field_for('identifiers', db.all_book_ids(tuple)) + existing_isbns = {normalize_isbn(ids.get('isbn', '')): book_id for book_id, ids in book_id_identifiers.items()} + existing_isbns.pop('', None) + ok = [] + duplicates = [] + for book in books: + q = normalize_isbn(book['isbn']) + if q and q in existing_isbns: + duplicates.append((book, existing_isbns[q])) + else: + ok.append(book) + if duplicates: + det_msg = '\n'.join(f'{book["isbn"]}: {db.field_for("title", book_id)}' for book, book_id in duplicates) + if question_dialog(self.gui, _('Duplicates found'), _( + 'Books with some of the specified ISBNs already exist in the calibre library.' + ' Click "Show details" for the full list. Do you want to add them anyway?'), det_msg=det_msg + ): + ok += [x[0] for x in duplicates] + return ok + + def add_isbns(self, books, add_tags=[], check_for_existing=False): + books = list(books) + if check_for_existing: + books = self.check_for_existing_isbns(books) + if not books: + return + self.isbn_books = books self.add_by_isbn_ids = set() self.isbn_add_tags = add_tags QTimer.singleShot(10, self.do_one_isbn_add) @@ -490,7 +517,7 @@ class AddAction(InterfaceAction): from calibre.gui2.dialogs.add_from_isbn import AddFromISBN d = AddFromISBN(self.gui) if d.exec_() == QDialog.DialogCode.Accepted and d.books: - self.add_isbns(d.books, add_tags=d.set_tags) + self.add_isbns(d.books, add_tags=d.set_tags, check_for_existing=d.check_for_existing) def add_books(self, *args): ''' diff --git a/src/calibre/gui2/dialogs/add_from_isbn.py b/src/calibre/gui2/dialogs/add_from_isbn.py index 27021204ba..b0941b8f5e 100644 --- a/src/calibre/gui2/dialogs/add_from_isbn.py +++ b/src/calibre/gui2/dialogs/add_from_isbn.py @@ -7,16 +7,15 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os - from qt.core import ( - QDialog, QApplication, QIcon, QVBoxLayout, QHBoxLayout, QDialogButtonBox, - QPlainTextEdit, QPushButton, QLabel, QLineEdit, Qt + QApplication, QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QIcon, QLabel, + QLineEdit, QPlainTextEdit, QPushButton, Qt, QVBoxLayout ) -from calibre.ebooks.metadata import check_isbn from calibre.constants import iswindows -from calibre.gui2 import gprefs, question_dialog, error_dialog -from polyglot.builtins import unicode_type, filter +from calibre.ebooks.metadata import check_isbn +from calibre.gui2 import error_dialog, gprefs, question_dialog +from polyglot.builtins import filter, unicode_type class AddFromISBN(QDialog): @@ -57,8 +56,8 @@ class AddFromISBN(QDialog): " create entries for books based on the ISBN and download metadata and covers for them.

\n" "

Any invalid ISBNs in the list will be ignored.

\n" "

You can also specify a file that will be added with each ISBN. To do this enter the full" - " path to the file after a >>. For example:

\n" - "

9788842915232 >> %s

"), self) + " path to the file after a >>. For example:

\n" + "

9788842915232 >> %s

"), self) l.addWidget(la), la.setWordWrap(True) l.addSpacing(20) self.la2 = la = QLabel(_("&Tags to set on created book entries:"), self) @@ -67,6 +66,10 @@ class AddFromISBN(QDialog): le.setText(', '.join(gprefs.get('add from ISBN tags', []))) la.setBuddy(le) l.addWidget(le) + self._check_for_existing = ce = QCheckBox(_('Check for books with the same ISBN already in library'), self) + ce.setChecked(gprefs.get('add from ISBN dup check', False)) + l.addWidget(ce) + l.addStretch(10) def paste(self, *args): @@ -78,10 +81,15 @@ class AddFromISBN(QDialog): new = old + '\n' + txt self.isbn_box.setPlainText(new) + @property + def check_for_existing(self): + return self._check_for_existing.isChecked() + def accept(self, *args): tags = unicode_type(self.add_tags.text()).strip().split(',') tags = list(filter(None, [x.strip() for x in tags])) gprefs['add from ISBN tags'] = tags + gprefs['add from ISBN dup check'] = self.check_for_existing self.set_tags = tags bad = set() for line in unicode_type(self.isbn_box.toPlainText()).strip().splitlines():