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
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:
9788842915232 >> %s
>>
. For example:\n"
+ "9788842915232 >> %s