Adding from ISBN: Add an option to check if there are existing books with the specified ISBNs already in the library. Fixes #1919103 [Enhancement Request: "Add from ISBN" duplicate check](https://bugs.launchpad.net/calibre/+bug/1919103)

This commit is contained in:
Kovid Goyal 2021-03-22 14:12:47 +05:30
parent 038163ac97
commit 4cd2abd3c1
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 85 additions and 36 deletions

View File

@ -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

View File

@ -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):
'''

View File

@ -7,16 +7,15 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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.</p>\n"
"<p>Any invalid ISBNs in the list will be ignored.</p>\n"
"<p>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 <code>>></code>. For example:</p>\n"
"<p><code>9788842915232 >> %s</code></p>"), self)
" path to the file after a <code>&gt;&gt;</code>. For example:</p>\n"
"<p><code>9788842915232 &gt;&gt; %s</code></p>"), 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():