From 29a4bc5442ea9296e9380e6c94ffdd363fd27791 Mon Sep 17 00:00:00 2001 From: Erez Zukerman <1092548+ezuk@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:22:26 -0400 Subject: [PATCH 1/3] feat: Improve Obok book selection dialog with search, checkboxes, and hide functionality The Obok SelectionDialog was a simple alphabetical list that became hard to navigate with large Kobo libraries. This adds three usability features: - Search box that live-filters by title, author, or series - Checkbox-based selection (replaces row-highlight) so selections persist across filter changes; Space key toggles the focused row - Manual hide system: "Hide Selected" persists hidden book IDs across sessions via plugin_prefs; "Show Hidden" reveals them again; books are auto-hidden when imported via OK - "In Library" toggle button filters to show only books whose titles already exist in the calibre library, for easy identification of duplicates to hide Replaces the old "All with DRM" / "All DRM free" buttons with "Select All", "Deselect All", "Hide Selected", "Show Hidden", and "In Library". --- Obok_plugin/action.py | 14 ++- Obok_plugin/config.py | 1 + Obok_plugin/dialogs.py | 210 +++++++++++++++++++++++++++++------------ 3 files changed, 162 insertions(+), 63 deletions(-) diff --git a/Obok_plugin/action.py b/Obok_plugin/action.py index 36562af5..65188868 100644 --- a/Obok_plugin/action.py +++ b/Obok_plugin/action.py @@ -131,8 +131,20 @@ def launchObok(self): showErrorDlg(msg, None) return + # Build a set of lowercased titles already in calibre library + calibre_titles = set() + try: + for book_id in self.db.all_book_ids(): + title = self.db.field_for('title', book_id) + if title: + calibre_titles.add(title.lower()) + except Exception: + debug_print("Failed to retrieve calibre library titles") + calibre_titles = set() + # Launch the Dialog so the user can select titles. - dlg = SelectionDialog(self.gui, self, books) + from calibre_plugins.obok_dedrm.config import plugin_prefs + dlg = SelectionDialog(self.gui, self, books, plugin_prefs, calibre_titles) if dlg.exec_(): books_to_import = dlg.getBooks() self.count = len(books_to_import) diff --git a/Obok_plugin/config.py b/Obok_plugin/config.py index fdfb424b..64f32995 100644 --- a/Obok_plugin/config.py +++ b/Obok_plugin/config.py @@ -13,6 +13,7 @@ plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask' plugin_prefs.defaults['kobo_serials'] = [] plugin_prefs.defaults['kobo_directory'] = u'' +plugin_prefs.defaults['hidden_books'] = [] from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION from calibre_plugins.obok_dedrm.utilities import (debug_print) diff --git a/Obok_plugin/dialogs.py b/Obok_plugin/dialogs.py index 16bfcb08..f7c1c3dc 100644 --- a/Obok_plugin/dialogs.py +++ b/Obok_plugin/dialogs.py @@ -9,13 +9,15 @@ LAB_DRM_FREE = '* : drm - free' try: - from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox, - QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog, - QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem) + from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox, + QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog, + QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem, + QLineEdit) except ImportError: - from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox, - QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog, - QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem) + from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox, + QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog, + QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem, + QLineEdit) try: from PyQt5.QtWidgets import (QListWidget, QAbstractItemView) @@ -44,17 +46,22 @@ class SelectionDialog(SizePersistedDialog): ''' Dialog to select the kobo books to decrypt ''' - def __init__(self, gui, interface_action, books): + def __init__(self, gui, interface_action, books, plugin_prefs, calibre_titles=None): ''' :param gui: Parent gui :param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py) :param books: list of Kobo book + :param plugin_prefs: JSONConfig plugin preferences (for persisting hidden books) + :param calibre_titles: set of lowercased titles already in calibre library ''' - + self.books = books self.gui = gui self.interface_action = interface_action - self.books = books + self.plugin_prefs = plugin_prefs + self.hidden_book_ids = set(plugin_prefs.get('hidden_books', [])) + self.calibre_titles = calibre_titles if calibre_titles is not None else set() + self.filter_in_library = False SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog') self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION)) @@ -73,12 +80,18 @@ def __init__(self, gui, interface_action, books): title_layout.setAlignment(Qt.AlignTop) layout.addSpacing(5) + + # Search box + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel(_('Search:'), self)) + self.search_box = QLineEdit(self) + self.search_box.setPlaceholderText(_('Filter by title, author, or series...')) + self.search_box.textChanged.connect(self._apply_filters) + search_layout.addWidget(self.search_box) + layout.addLayout(search_layout) + main_layout = QHBoxLayout() layout.addLayout(main_layout) -# self.listy = QListWidget() -# self.listy.setSelectionMode(QAbstractItemView.ExtendedSelection) -# main_layout.addWidget(self.listy) -# self.listy.addItems(books) self.books_table = BookListTableWidget(self) main_layout.addWidget(self.books_table) @@ -87,28 +100,57 @@ def __init__(self, gui, interface_action, books): button_box.accepted.connect(self._ok_clicked) button_box.rejected.connect(self.reject) self.select_all_button = button_box.addButton(_("Select All"), QDialogButtonBox.ResetRole) - self.select_all_button.setToolTip(_("Select all books to add them to the calibre library.")) + self.select_all_button.setToolTip(_("Select all visible books.")) self.select_all_button.clicked.connect(self._select_all_clicked) - self.select_drm_button = button_box.addButton(_("All with DRM"), QDialogButtonBox.ResetRole) - self.select_drm_button.setToolTip(_("Select all books with DRM.")) - self.select_drm_button.clicked.connect(self._select_drm_clicked) - self.select_free_button = button_box.addButton(_("All DRM free"), QDialogButtonBox.ResetRole) - self.select_free_button.setToolTip(_("Select all books without DRM.")) - self.select_free_button.clicked.connect(self._select_free_clicked) + self.deselect_all_button = button_box.addButton(_("Deselect All"), QDialogButtonBox.ResetRole) + self.deselect_all_button.setToolTip(_("Deselect all books.")) + self.deselect_all_button.clicked.connect(self._deselect_all_clicked) + self.hide_button = button_box.addButton(_("Hide Selected"), QDialogButtonBox.ResetRole) + self.hide_button.setToolTip(_("Hide checked books from this list. They won't appear next time.")) + self.hide_button.clicked.connect(self._hide_selected_clicked) + self.show_hidden_button = button_box.addButton(_("Show Hidden"), QDialogButtonBox.ResetRole) + self.show_hidden_button.setToolTip(_("Show all previously hidden books.")) + self.show_hidden_button.clicked.connect(self._show_hidden_clicked) + self.in_library_button = button_box.addButton(_("In Library"), QDialogButtonBox.ResetRole) + self.in_library_button.setToolTip(_("Toggle: show only books whose titles are already in your calibre library.")) + self.in_library_button.setCheckable(True) + self.in_library_button.clicked.connect(self._in_library_clicked) layout.addWidget(button_box) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() self.books_table.populate_table(self.books) + self._apply_filters() + + def _apply_filters(self): + search_text = self.search_box.text().lower() + self.books_table.apply_filters(search_text, self.hidden_book_ids, + self.filter_in_library, self.calibre_titles) + + def _in_library_clicked(self): + self.filter_in_library = self.in_library_button.isChecked() + self._apply_filters() + + def _save_hidden_books(self): + self.plugin_prefs['hidden_books'] = list(self.hidden_book_ids) + + def _hide_selected_clicked(self): + new_ids = self.books_table.get_checked_volumeids() + self.hidden_book_ids.update(new_ids) + self._save_hidden_books() + self.books_table.deselect_all() + self._apply_filters() + + def _show_hidden_clicked(self): + self.hidden_book_ids.clear() + self._save_hidden_books() + self._apply_filters() def _select_all_clicked(self): self.books_table.select_all() - def _select_drm_clicked(self): - self.books_table.select_drm(True) - - def _select_free_clicked(self): - self.books_table.select_drm(False) + def _deselect_all_clicked(self): + self.books_table.deselect_all() def _help_link_activated(self, url): ''' @@ -120,7 +162,12 @@ def _ok_clicked(self): ''' Build an index of the selected titles ''' - if len(self.books_table.selectedItems()): + books = self.books_table.get_books() + if len(books): + # Auto-hide the books we're about to import + for book in books: + self.hidden_book_ids.add(book.volumeid) + self._save_hidden_books() self.accept() else: msg = 'You must make a selection!' @@ -137,13 +184,14 @@ class BookListTableWidget(QTableWidget): def __init__(self, parent): QTableWidget.__init__(self, parent) + self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) def populate_table(self, books): self.clear() self.setAlternatingRowColors(True) self.setRowCount(len(books)) - header_labels = ['DRM', _('Title'), _('Author'), _('Series'), 'book_id'] + header_labels = ['', 'DRM', _('Title'), _('Author'), _('Series'), 'book_id'] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.verticalHeader().setDefaultSectionSize(24) @@ -156,12 +204,11 @@ def populate_table(self, books): self.setSortingEnabled(False) self.resizeColumnsToContents() - self.setMinimumColumnWidth(1, 100) + self.setColumnWidth(0, 30) self.setMinimumColumnWidth(2, 100) + self.setMinimumColumnWidth(3, 100) self.setMinimumSize(300, 0) - if len(books) > 0: - self.selectRow(0) - self.hideColumn(4) + self.hideColumn(5) self.setSortingEnabled(True) def setMinimumColumnWidth(self, col, minimum): @@ -169,53 +216,92 @@ def setMinimumColumnWidth(self, col, minimum): self.setColumnWidth(col, minimum) def populate_table_row(self, row, book): + # Column 0: Checkbox + check_item = QTableWidgetItem() + check_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + check_item.setCheckState(Qt.Unchecked) + check_item.setData(Qt.UserRole, book.volumeid) + self.setItem(row, 0, check_item) + + # Column 1: DRM icon if book.has_drm: icon = get_icon('drm-locked.png') val = 1 else: icon = get_icon('drm-unlocked.png') val = 0 - status_cell = IconWidgetItem(None, icon, val) status_cell.setData(Qt.UserRole, val) - self.setItem(row, 0, status_cell) - self.setItem(row, 1, ReadOnlyTableWidgetItem(book.title)) - self.setItem(row, 2, AuthorTableWidgetItem(book.author, book.author)) - self.setItem(row, 3, SeriesTableWidgetItem(book.series, book.series_index)) - self.setItem(row, 4, NumericTableWidgetItem(row)) + self.setItem(row, 1, status_cell) + + # Column 2: Title (store lowercased title in UserRole for search filtering) + title_item = ReadOnlyTableWidgetItem(book.title) + title_item.setData(Qt.UserRole, book.title.lower()) + self.setItem(row, 2, title_item) + + # Column 3: Author + self.setItem(row, 3, AuthorTableWidgetItem(book.author, book.author)) + + # Column 4: Series + self.setItem(row, 4, SeriesTableWidgetItem(book.series, book.series_index)) + + # Column 5: book_id (hidden) + self.setItem(row, 5, NumericTableWidgetItem(row)) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Space: + row = self.currentRow() + if row >= 0 and not self.isRowHidden(row): + item = self.item(row, 0) + if item.checkState() == Qt.Checked: + item.setCheckState(Qt.Unchecked) + else: + item.setCheckState(Qt.Checked) + else: + QTableWidget.keyPressEvent(self, event) def get_books(self): -# debug_print("BookListTableWidget:get_books - self.books:", self.books) books = [] - if len(self.selectedItems()): - for row in range(self.rowCount()): -# debug_print("BookListTableWidget:get_books - row:", row) - if self.item(row, 0).isSelected(): - book_num = convert_qvariant(self.item(row, 4).data(Qt.DisplayRole)) - debug_print("BookListTableWidget:get_books - book_num:", book_num) - book = self.books[book_num] - debug_print("BookListTableWidget:get_books - book:", book.title) - books.append(book) + for row in range(self.rowCount()): + if self.item(row, 0).checkState() == Qt.Checked: + book_num = convert_qvariant(self.item(row, 5).data(Qt.DisplayRole)) + book = self.books[book_num] + books.append(book) return books + def get_checked_volumeids(self): + ids = set() + for row in range(self.rowCount()): + if self.item(row, 0).checkState() == Qt.Checked: + ids.add(self.item(row, 0).data(Qt.UserRole)) + return ids + def select_all(self): - self .selectAll() + for row in range(self.rowCount()): + if not self.isRowHidden(row): + self.item(row, 0).setCheckState(Qt.Checked) - def select_drm(self, has_drm): - self.clearSelection() - current_selection_mode = self.selectionMode() - self.setSelectionMode(QAbstractItemView.MultiSelection) + def deselect_all(self): for row in range(self.rowCount()): -# debug_print("BookListTableWidget:select_drm - row:", row) - if convert_qvariant(self.item(row, 0).data(Qt.UserRole)) == 1: -# debug_print("BookListTableWidget:select_drm - has DRM:", row) - if has_drm: - self.selectRow(row) - else: -# debug_print("BookListTableWidget:select_drm - DRM free:", row) - if not has_drm: - self.selectRow(row) - self.setSelectionMode(current_selection_mode) + self.item(row, 0).setCheckState(Qt.Unchecked) + + def apply_filters(self, search_text, hidden_book_ids, filter_in_library=False, calibre_titles=None): + calibre_titles = calibre_titles or set() + for row in range(self.rowCount()): + volumeid = self.item(row, 0).data(Qt.UserRole) + title_lower = self.item(row, 2).data(Qt.UserRole) + author_text = self.item(row, 3).text().lower() if self.item(row, 3).text() else '' + series_text = self.item(row, 4).text().lower() if self.item(row, 4).text() else '' + + hidden_by_search = bool(search_text) and ( + search_text not in title_lower and + search_text not in author_text and + search_text not in series_text + ) + hidden_by_user = volumeid in hidden_book_ids + hidden_by_library = filter_in_library and (title_lower not in calibre_titles) + + self.setRowHidden(row, hidden_by_search or hidden_by_user or hidden_by_library) class DecryptAddProgressDialog(QProgressDialog): From a3a10ad573d71802e59398bc3787241b9371edb3 Mon Sep 17 00:00:00 2001 From: Erez Zukerman <1092548+ezuk@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:25:16 -0400 Subject: [PATCH 2/3] the plugin zip for my own use --- Obok_DeDRM_plugin.zip | Bin 0 -> 74944 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Obok_DeDRM_plugin.zip diff --git a/Obok_DeDRM_plugin.zip b/Obok_DeDRM_plugin.zip new file mode 100644 index 0000000000000000000000000000000000000000..cb3c5d488e741811c798645a79c08106076df940 GIT binary patch literal 74944 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

YajnPhsmyQLj!lHuhifCUZG?N3l}(%RKy+Vdj^ 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 Date: Thu, 25 Jun 2026 11:55:57 -0400 Subject: [PATCH 3/3] feat: make Obok in-library detection author-first and drift-tolerant The "In Library" filter relied on exact lowercased title equality, which silently missed a large share of duplicates as Kobo and calibre metadata drifted apart. Rework matching into a dedicated, unit-tested module. - Add Obok_plugin/title_match.py: title normalization (smart quotes, &, subtitle/parenthetical/edition handling) plus a LibraryMatcher that is author-first -- confirm the author(s) match (token-based; tolerant of order, "Last, First", separators; ignores ISBN-as-author junk; matches on a shared strong name token), then harmonize titles (exact form set, leading/trailing article, bounded Levenshtein fuzzy). - dialogs.py: precompute in-library status once (not per keystroke); add a "Not in Library" toggle and open to it by default (the import-eligible diff); move verbose match diagnostics behind calibre's DEBUG flag. - action.py: build the matcher from all calibre authors, not just the first. - obok/obok.py: use raw strings for the MAC-address regexes to silence Python 3.12+ invalid-escape SyntaxWarnings (no behavior change). - Add tests/test_obok_title_match.py (30 cases) and repo-root pytest.ini; rebuild the bundled plugin zip. --- .gitignore | 5 +- Obok_DeDRM_plugin.zip | Bin 74944 -> 83486 bytes Obok_plugin/action.py | 29 ++- Obok_plugin/dialogs.py | 88 ++++++++- Obok_plugin/obok/obok.py | 10 +- Obok_plugin/title_match.py | 343 +++++++++++++++++++++++++++++++++ pytest.ini | 6 + tests/test_obok_title_match.py | 237 +++++++++++++++++++++++ 8 files changed, 694 insertions(+), 24 deletions(-) create mode 100644 Obok_plugin/title_match.py create mode 100644 pytest.ini create mode 100644 tests/test_obok_title_match.py diff --git a/.gitignore b/.gitignore index f3f3d471..f4e159f8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ # Cache /DeDRM_plugin/__pycache__ -/DeDRM_plugin/standalone/__pycache__ \ No newline at end of file +/DeDRM_plugin/standalone/__pycache__ +__pycache__/ +*.pyc +.pytest_cache/ \ No newline at end of file diff --git a/Obok_DeDRM_plugin.zip b/Obok_DeDRM_plugin.zip index cb3c5d488e741811c798645a79c08106076df940..f18c4d7dddd76be78f763a67167722ebd44457e5 100644 GIT binary patch delta 24671 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

  • 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 diff --git a/Obok_plugin/action.py b/Obok_plugin/action.py index 65188868..b8af3af9 100644 --- a/Obok_plugin/action.py +++ b/Obok_plugin/action.py @@ -34,6 +34,7 @@ from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok +from calibre_plugins.obok_dedrm.title_match import LibraryMatcher PLUGIN_ICONS = ['images/obok.png'] @@ -131,20 +132,32 @@ def launchObok(self): showErrorDlg(msg, None) return - # Build a set of lowercased titles already in calibre library - calibre_titles = set() + # Build a matcher over the titles already in the calibre library so the + # dialog can flag (and filter) Kobo books that are already imported. + # Kobo and calibre store titles independently, so LibraryMatcher + # normalizes both sides and falls back to a bounded fuzzy match. try: - for book_id in self.db.all_book_ids(): + book_ids = self.db.all_book_ids() + entries = [] + for book_id in book_ids: title = self.db.field_for('title', book_id) - if title: - calibre_titles.add(title.lower()) + authors = self.db.field_for('authors', book_id) + if isinstance(authors, (tuple, list)): + author = ' & '.join(a for a in authors if a) + else: + author = authors or '' + entries.append((title, author)) + matcher = LibraryMatcher(entries) + debug_print("OBOK DIAG: built library matcher from %d calibre books (%d unique normalized titles)" + % (len(book_ids), len(matcher))) except Exception: - debug_print("Failed to retrieve calibre library titles") - calibre_titles = set() + debug_print("OBOK DIAG: FAILED to read calibre library titles -- traceback follows") + traceback.print_exc() + matcher = LibraryMatcher([]) # Launch the Dialog so the user can select titles. from calibre_plugins.obok_dedrm.config import plugin_prefs - dlg = SelectionDialog(self.gui, self, books, plugin_prefs, calibre_titles) + dlg = SelectionDialog(self.gui, self, books, plugin_prefs, matcher) if dlg.exec_(): books_to_import = dlg.getBooks() self.count = len(books_to_import) diff --git a/Obok_plugin/dialogs.py b/Obok_plugin/dialogs.py index f7c1c3dc..9e9d81e2 100644 --- a/Obok_plugin/dialogs.py +++ b/Obok_plugin/dialogs.py @@ -24,6 +24,7 @@ except ImportError: from PyQt4.QtGui import (QListWidget, QAbstractItemView) +from calibre.constants import DEBUG from calibre.gui2 import gprefs, warning_dialog, error_dialog from calibre.gui2.dialogs.message_box import MessageBox @@ -34,6 +35,7 @@ ) from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION) +from calibre_plugins.obok_dedrm.title_match import LibraryMatcher, title_keys try: debug_print("obok::dialogs.py - loading translations") @@ -46,13 +48,13 @@ class SelectionDialog(SizePersistedDialog): ''' Dialog to select the kobo books to decrypt ''' - def __init__(self, gui, interface_action, books, plugin_prefs, calibre_titles=None): + def __init__(self, gui, interface_action, books, plugin_prefs, matcher=None): ''' :param gui: Parent gui :param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py) :param books: list of Kobo book :param plugin_prefs: JSONConfig plugin preferences (for persisting hidden books) - :param calibre_titles: set of lowercased titles already in calibre library + :param matcher: LibraryMatcher over titles already in the calibre library ''' self.books = books @@ -60,8 +62,21 @@ def __init__(self, gui, interface_action, books, plugin_prefs, calibre_titles=No self.interface_action = interface_action self.plugin_prefs = plugin_prefs self.hidden_book_ids = set(plugin_prefs.get('hidden_books', [])) - self.calibre_titles = calibre_titles if calibre_titles is not None else set() - self.filter_in_library = False + self.matcher = matcher if matcher is not None else LibraryMatcher([]) + # Default to the import-eligible diff (books not yet in calibre); the + # 'Not in Library' button below is checked to match this on open. + self.library_filter_mode = 'not_in' # None | 'in' | 'not_in' + + # Precompute, once, which Kobo books are already in the calibre library. + # Done here -- not in apply_filters -- so the fuzzy scan does not re-run + # on every keystroke / filter toggle. + self.in_library_ids = set() + for book in self.books: + if self.matcher.match(book.title, book.author) is not None: + self.in_library_ids.add(book.volumeid) + + if DEBUG: + self._log_match_diagnostics() SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog') self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION)) @@ -112,9 +127,14 @@ def __init__(self, gui, interface_action, books, plugin_prefs, calibre_titles=No self.show_hidden_button.setToolTip(_("Show all previously hidden books.")) self.show_hidden_button.clicked.connect(self._show_hidden_clicked) self.in_library_button = button_box.addButton(_("In Library"), QDialogButtonBox.ResetRole) - self.in_library_button.setToolTip(_("Toggle: show only books whose titles are already in your calibre library.")) + self.in_library_button.setToolTip(_("Toggle: show only books already in your calibre library.")) self.in_library_button.setCheckable(True) self.in_library_button.clicked.connect(self._in_library_clicked) + self.not_in_library_button = button_box.addButton(_("Not in Library"), QDialogButtonBox.ResetRole) + self.not_in_library_button.setToolTip(_("Toggle: show only books NOT yet in your calibre library (eligible to import).")) + self.not_in_library_button.setCheckable(True) + self.not_in_library_button.clicked.connect(self._not_in_library_clicked) + self.not_in_library_button.setChecked(True) # default view: not-in-library layout.addWidget(button_box) # Cause our dialog size to be restored from prefs or created on first usage @@ -122,13 +142,55 @@ def __init__(self, gui, interface_action, books, plugin_prefs, calibre_titles=No self.books_table.populate_table(self.books) self._apply_filters() + def _log_match_diagnostics(self): + ''' + Verbose in-library match breakdown. Only called when calibre is run with + DEBUG on (e.g. ``calibre-debug -g``); silent and skipped in normal use. + ''' + in_lib_exact = in_lib_approx = 0 + author_mismatch = [] + for book in self.books: + hit = self.matcher.match(book.title, book.author) + if hit is not None: + if hit in title_keys(book.title): + in_lib_exact += 1 + else: + in_lib_approx += 1 + debug_print("OBOK DIAG: approximate in-library match: %r (%r) ~= calibre %r" + % (book.title, book.author, hit)) + elif self.matcher.title_exists(book.title): + # Title is in the library but the author didn't match: the + # author-first gate is treating it as a different book. + author_mismatch.append((book.title, book.author)) + debug_print("OBOK DIAG: %d Kobo books; in-library exact=%d approx=%d; not-in-library=%d" + % (len(self.books), in_lib_exact, in_lib_approx, + len(self.books) - len(self.in_library_ids))) + if author_mismatch: + debug_print("OBOK DIAG: %d not-in-library books whose TITLE exists under a different author:" + % len(author_mismatch)) + for t, a in author_mismatch[:15]: + debug_print("OBOK DIAG: author-mismatch: %r (Kobo author: %r)" % (t, a)) + def _apply_filters(self): search_text = self.search_box.text().lower() self.books_table.apply_filters(search_text, self.hidden_book_ids, - self.filter_in_library, self.calibre_titles) + self.library_filter_mode, self.in_library_ids) def _in_library_clicked(self): - self.filter_in_library = self.in_library_button.isChecked() + # 'In Library' and 'Not in Library' are mutually exclusive views. + if self.in_library_button.isChecked(): + self.not_in_library_button.setChecked(False) + self.library_filter_mode = 'in' + else: + self.library_filter_mode = None + self._apply_filters() + + def _not_in_library_clicked(self): + if self.not_in_library_button.isChecked(): + self.in_library_button.setChecked(False) + self.library_filter_mode = 'not_in' + else: + self.library_filter_mode = None self._apply_filters() def _save_hidden_books(self): @@ -285,8 +347,8 @@ def deselect_all(self): for row in range(self.rowCount()): self.item(row, 0).setCheckState(Qt.Unchecked) - def apply_filters(self, search_text, hidden_book_ids, filter_in_library=False, calibre_titles=None): - calibre_titles = calibre_titles or set() + def apply_filters(self, search_text, hidden_book_ids, library_filter_mode=None, in_library_ids=None): + in_library_ids = in_library_ids or set() for row in range(self.rowCount()): volumeid = self.item(row, 0).data(Qt.UserRole) title_lower = self.item(row, 2).data(Qt.UserRole) @@ -299,7 +361,13 @@ def apply_filters(self, search_text, hidden_book_ids, filter_in_library=False, c search_text not in series_text ) hidden_by_user = volumeid in hidden_book_ids - hidden_by_library = filter_in_library and (title_lower not in calibre_titles) + in_lib = volumeid in in_library_ids + if library_filter_mode == 'in': + hidden_by_library = not in_lib + elif library_filter_mode == 'not_in': + hidden_by_library = in_lib + else: + hidden_by_library = False self.setRowHidden(row, hidden_by_search or hidden_by_user or hidden_by_library) diff --git a/Obok_plugin/obok/obok.py b/Obok_plugin/obok/obok.py index a8379b1c..44480fd3 100644 --- a/Obok_plugin/obok/obok.py +++ b/Obok_plugin/obok/obok.py @@ -429,7 +429,7 @@ def __getmacaddrs (self): """The list of all MAC addresses on this machine.""" macaddrs = [] if sys.platform.startswith('win'): - c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) + c = re.compile(r'\s?(' + r'[0-9a-f]{2}[:\-]' * 5 + r'[0-9a-f]{2})(\s|$)', re.IGNORECASE) try: output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout for line in output: @@ -443,7 +443,7 @@ def __getmacaddrs (self): if m: macaddrs.append(re.sub("-", ":", m.group(1)).upper()) elif sys.platform.startswith('darwin'): - c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) + c = re.compile(r'\s(' + '[0-9a-f]{2}:' * 5 + r'[0-9a-f]{2})(\s|$)', re.IGNORECASE) output = subprocess.check_output('/sbin/ifconfig -a', shell=True, encoding='utf-8') matches = c.findall(output) for m in matches: @@ -459,14 +459,14 @@ def __getmacaddrs (self): else: # final fallback # let's try ip - c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) + c = re.compile(r'\s(' + '[0-9a-f]{2}:' * 5 + r'[0-9a-f]{2})(\s|$)', re.IGNORECASE) for line in os.popen('ip -br link'): m = c.search(line) if m: macaddrs.append(m.group(1).upper()) # let's try ipconfig under wine - c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) + c = re.compile(r'\s(' + '[0-9a-f]{2}-' * 5 + r'[0-9a-f]{2})(\s|$)', re.IGNORECASE) for line in os.popen('ipconfig /all'): m = c.search(line) if m: @@ -665,7 +665,7 @@ def decrypt_book(book, lib): print("Converting {0}".format(book.title)) zin = zipfile.ZipFile(book.filename, "r") # make filename out of Unicode alphanumeric and whitespace equivalents from title - outname = "{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE)) + outname = "{0}.epub".format(re.sub(r'[^\s\w]', '_', book.title, 0, re.UNICODE)) if (book.type == 'drm-free'): print("DRM-free book, conversion is not needed") shutil.copyfile(book.filename, outname) diff --git a/Obok_plugin/title_match.py b/Obok_plugin/title_match.py new file mode 100644 index 00000000..7c7c0167 --- /dev/null +++ b/Obok_plugin/title_match.py @@ -0,0 +1,343 @@ +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__docformat__ = 'restructuredtext en' + +''' +Title matching between Kobo books and the calibre library. + +Kobo and calibre maintain a book's title independently, so a plain exact +string compare misses a large fraction of real duplicates: + + * smart vs straight quotes/apostrophes ("All the King's Men") + * a subtitle present on one side only ("Born a Crime: Stories ...") + * a trailing parenthetical / edition tag ("... (Pulitzer Prize Winner)", + "... 10th Anniversary") + * "&" vs "and" + +`normalize_title` canonicalizes a title so the two sides can be compared +reliably; it MUST be applied identically to both sides. `LibraryMatcher` +wraps the calibre side and, when the normalized forms don't match exactly, +falls back to a bounded Levenshtein (edit-distance) similarity check to catch +typos and minor variations. + +This module deliberately has no calibre / Qt imports so it can be unit tested +on its own (see tests/test_title_match.py). +''' + +import re +import unicodedata + +# Punctuation that differs between Kobo and calibre but should be treated as +# equivalent. Folded before the (lossy) generic punctuation pass below so that +# colon/paren detection still works on already-folded text. +_QUOTE_FOLDS = { + '‘': "'", '’': "'", '‚': "'", '‛': "'", + '´': "'", '`': "'", + '“': '"', '”': '"', '„': '"', '‟': '"', + '–': '-', '—': '-', '‒': '-', '―': '-', '−': '-', +} + +# A trailing parenthetical/bracketed group, e.g. " (Pulitzer Prize Winner)". +_TRAILING_BRACKET_RE = re.compile(r'\s*[\(\[\{][^\(\)\[\]\{\}]*[\)\]\}]\s*$') + +# A trailing edition descriptor, e.g. " 10th Anniversary", " Deluxe Edition". +# "anniversary" may stand alone (rarely a real title word); the others must be +# followed by "edition" to avoid stripping legitimate title words. +_EDITION_SUFFIX_RE = re.compile( + r'\s+(?:' + r'(?:\d+\s*(?:st|nd|rd|th)\s+)?anniversary(?:\s+edition)?' + r'|(?:deluxe|revised|expanded|updated|illustrated|annotated|special' + r'|definitive|collector\'?s?|unabridged|abridged|reprint)\s+edition' + r')\s*$' +) + +_NON_ALNUM_RE = re.compile(r'[^\w\s]', re.UNICODE) +_WHITESPACE_RE = re.compile(r'\s+') + +# An English article at the front ("The Hobbit") or moved to the end in +# calibre's sort form ("Hobbit, The", which normalizes to a trailing " the"). +_LEADING_ARTICLE_RE = re.compile(r'^(?:the|a|an)\s+') +_TRAILING_ARTICLE_RE = re.compile(r'\s+(?:the|a|an)$') + +# Fuzzy-match tuning. Kept as module constants so they're trivial to adjust +# from observed behaviour (the dialog logs every fuzzy match it makes). +FUZZY_THRESHOLD = 0.87 # minimum similarity (1.0 == identical) to accept +FUZZY_MIN_LEN = 6 # don't fuzzy-match titles shorter than this + + +def normalize_title(title, strip_subtitle=True): + ''' + Canonicalize a book title for comparison. Returns a lowercased, punctuation + -folded string with parenthetical / edition noise removed. When + `strip_subtitle` is true (default) the part after the first colon is also + dropped. Prefer `title_keys` for matching, which considers both forms. + ''' + if not title: + return '' + try: + s = unicodedata.normalize('NFKC', title) + except TypeError: + # py2 bytes safety net; modern calibre passes unicode. + s = unicodedata.normalize('NFKC', title.decode('utf-8', 'replace')) + for src, dst in _QUOTE_FOLDS.items(): + if src in s: + s = s.replace(src, dst) + s = s.lower() + s = s.replace('&', ' and ') + if strip_subtitle: + # Drop subtitle after the first colon ("Born a Crime: Stories ..." -> ...). + s = s.split(':', 1)[0] + # Strip trailing parenthetical/bracketed groups (possibly nested/repeated). + prev = None + while prev != s: + prev = s + s = _TRAILING_BRACKET_RE.sub('', s) + # Strip a trailing edition descriptor. + s = _EDITION_SUFFIX_RE.sub('', s) + # Fold any remaining punctuation (apostrophes, hyphens, %, ...) to spaces. + s = _NON_ALNUM_RE.sub(' ', s) + s = _WHITESPACE_RE.sub(' ', s).strip() + return s + + +def title_keys(title): + ''' + The set of normalized forms a title can match on: the subtitle-stripped form + AND the full form (colon kept as a separator). Comparing the sets on both + sides means "Foo: Bar" matches a bare "Foo" *and* a colon-less "Foo Bar". + ''' + keys = set() + stripped = normalize_title(title, strip_subtitle=True) + if stripped: + keys.add(stripped) + full = normalize_title(title, strip_subtitle=False) + if full: + keys.add(full) + return keys + + +def _article_key(normalized): + ''' + Reduce an already-normalized title to an article-insensitive key by removing + an article at the front ("the hobbit") or the end (calibre's "hobbit, the", + which normalizes to a trailing " the"). + ''' + s = _LEADING_ARTICLE_RE.sub('', normalized) + s = _TRAILING_ARTICLE_RE.sub('', s) + return s.strip() + + +def normalize_author(author): + ''' + Canonicalize an author for comparison. Lowercases, drops punctuation, and + sorts the name tokens so display order / "Last, First" don't matter + ("Robert Penn Warren" == "Warren, Robert Penn"). Returns '' if unknown. + ''' + if not author: + return '' + try: + s = unicodedata.normalize('NFKC', author) + except TypeError: + s = unicodedata.normalize('NFKC', author.decode('utf-8', 'replace')) + s = s.lower() + s = _NON_ALNUM_RE.sub(' ', s) + return ' '.join(sorted(s.split())) + + +def _strong_tokens(na): + ''' + The distinctive tokens of a normalized author string: alphabetic name parts + of length >= 3. Drops single initials ("j") and purely numeric junk (an + ISBN or year that crept into the author field, as calibre sometimes stores). + ''' + return frozenset(t for t in na.split() if len(t) >= 3 and not t.isdigit()) + + +def _bounded_levenshtein(a, b, max_dist): + ''' + Levenshtein edit distance between `a` and `b`, but returns `max_dist + 1` + as soon as the distance is guaranteed to exceed `max_dist`. The early-out + keeps the per-comparison cost low when scanning a large library. + ''' + la, lb = len(a), len(b) + if abs(la - lb) > max_dist: + return max_dist + 1 + if la == 0: + return lb + if lb == 0: + return la + if la > lb: # keep the inner row short + a, b = b, a + la, lb = lb, la + previous = list(range(la + 1)) + for j in range(1, lb + 1): + current = [j] + [0] * la + bj = b[j - 1] + row_min = current[0] + for i in range(1, la + 1): + cost = 0 if a[i - 1] == bj else 1 + current[i] = min(previous[i] + 1, + current[i - 1] + 1, + previous[i - 1] + cost) + if current[i] < row_min: + row_min = current[i] + if row_min > max_dist: + return max_dist + 1 + previous = current + return previous[la] + + +class _AuthorGroup(object): + '''Title keys for all library books by one (normalized) author.''' + __slots__ = ('titles', 'article') + + def __init__(self): + self.titles = set() # every title key by this author + self.article = {} # article key -> title key + + def add(self, keys): + self.titles.update(keys) + for k in keys: + self.article[_article_key(k)] = k + + +class LibraryMatcher(object): + ''' + Decides whether a (Kobo) book is already present in the calibre library. + + A book is the same when the AUTHOR(S) agree *and* the title harmonizes. + Author agreement is checked first: authors are normalized to a set of name + tokens, tolerant of order, "Last, First", separators, and one side listing + extra authors (subset/superset). Only within a matching author's books are + titles compared -- exact normalized form, then leading/trailing article, + then bounded fuzzy. A book with no usable author falls back to an exact + full-title match only (no fuzzy). Treat as immutable after construction. + ''' + + def __init__(self, entries, fuzzy=True, + threshold=FUZZY_THRESHOLD, min_len=FUZZY_MIN_LEN): + ''' + :param entries: iterable of calibre books; each item is either a title + string or a ``(title, author)`` pair. + ''' + self._fuzzy = fuzzy + self._threshold = threshold + self._min_len = min_len + self._all_titles = set() # every title key (for the no-author fallback) + self._groups = {} # normalized author -> _AuthorGroup + self._token_to_authors = {} # strong author token -> set of normalized authors + for entry in entries: + if isinstance(entry, (tuple, list)): + title = entry[0] + author = entry[1] if len(entry) > 1 else '' + else: + title, author = entry, '' + keys = title_keys(title) + if not keys: + continue + self._all_titles.update(keys) + na = normalize_author(author) + if not na: + continue + group = self._groups.get(na) + if group is None: + group = self._groups[na] = _AuthorGroup() + for tok in _strong_tokens(na): + self._token_to_authors.setdefault(tok, set()).add(na) + group.add(keys) + + def __len__(self): + return len(self._all_titles) + + def title_exists(self, title): + ''' + True if the title alone (ignoring author) matches some library book. + For diagnostics: title_exists() True while match() is None means the + title is present but under a different author. + ''' + return bool(title_keys(title) & self._all_titles) + + def contains(self, title, author=None): + '''True if `title` by `author` matches a book already in the library.''' + return self.match(title, author) is not None + + def match(self, title, author=None): + ''' + Return the normalized calibre title that `title` (by `author`) matches, + or None. Author is checked first; within a matching author, the title + is harmonized (exact form -> leading/trailing article -> bounded fuzzy). + (Returning the match -- not just a bool -- lets callers log *why* + something was considered a duplicate.) + ''' + keys = title_keys(title) + if not keys: + return None + na = normalize_author(author) + if not na: + # Author unknown: only trust an exact title match, nothing looser. + for k in keys: + if k in self._all_titles: + return k + return None + for group in self._candidate_groups(na): + # exact title within this author + for k in keys: + if k in group.titles: + return k + # same author, article-insensitive ("The X" / "X" / "X, The") + for k in keys: + hit = group.article.get(_article_key(k)) + if hit is not None: + return hit + # same author, bounded fuzzy title + hit = self._fuzzy_title(keys, group.titles) + if hit is not None: + return hit + return None + + def _candidate_groups(self, na): + ''' + Yield author groups that plausibly denote the same author as `na`: the + exact author first, then any author sharing a strong name token (see + `_strong_tokens`). This tolerates honorifics / ordination names ("Bhante" + vs "Henepola Gunaratana"), abbreviated middles ("Oren J." vs "Oren Jay"), + one side listing extra authors, and junk tokens like an ISBN calibre + stored as a second author. The caller's title check keeps it honest. + ''' + seen = set() + exact = self._groups.get(na) + if exact is not None: + seen.add(na) + yield exact + candidates = set() + for tok in _strong_tokens(na): + candidates |= self._token_to_authors.get(tok, set()) + for other_na in candidates: + if other_na in seen: + continue + seen.add(other_na) + yield self._groups[other_na] + + def _fuzzy_title(self, keys, titles): + if not self._fuzzy: + return None + n = max(keys, key=len) + if len(n) < self._min_len: + return None + ln = len(n) + threshold = self._threshold + for cand in titles: + lc = len(cand) + longest = ln if ln > lc else lc + if longest == 0: + continue + # Cheap upper bound on similarity from the length gap alone. + if (1.0 - (abs(ln - lc) / float(longest))) < threshold: + continue + max_dist = int((1.0 - threshold) * longest) + if _bounded_levenshtein(n, cand, max_dist) <= max_dist: + return cand + return None \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..477707b7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +# Obok_plugin/ is an importable package whose __init__.py needs the calibre +# runtime, so tests live at the repo root (outside any package) and import the +# pure title_match module directly via sys.path. +testpaths = tests +addopts = --import-mode=importlib diff --git a/tests/test_obok_title_match.py b/tests/test_obok_title_match.py new file mode 100644 index 00000000..d8bbd076 --- /dev/null +++ b/tests/test_obok_title_match.py @@ -0,0 +1,237 @@ +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +''' +Unit tests for title_match.normalize_title / LibraryMatcher. + +These use the real Kobo titles surfaced by the diagnostic build against +Erez's library (the ones that exact-matching missed), so the tests document +the actual failure modes we're fixing. + +Run from the repo root: pytest -v (or: python3 -m pytest -v) +''' +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +import os +import sys + +# title_match lives inside the Obok_plugin package dir; add it to the path so we +# can import the module without importing the package (whose __init__ needs calibre). +sys.path.insert(0, os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'Obok_plugin')) + +from title_match import normalize_title, normalize_author, LibraryMatcher # noqa: E402 + +CURLY = '’' # right single quotation mark (Kobo's apostrophe) + + +def n(t): + return normalize_title(t) + + +# --- normalize_title: the three observed drift buckets ----------------------- + +def test_smart_apostrophe_folds_to_straight(): + # Kobo stores a curly apostrophe; calibre a straight one. + assert n('All The King' + CURLY + 's Men') == n("All the King's Men") + assert n('Assassin' + CURLY + 's Apprentice') == n("Assassin's Apprentice") + assert n('Cat' + CURLY + 's Cradle') == n("Cat's Cradle") + + +def test_subtitle_after_colon_is_dropped(): + assert n('Born a Crime') == n('Born a Crime: Stories from a South African Childhood') + assert n('Be Useful') == n('Be Useful: Seven Tools for Life') + assert n('Co-Intelligence') == n('Co-Intelligence: Living and Working with AI') + + +def test_trailing_parenthetical_is_dropped(): + assert n('American Prometheus (Pulitzer Prize Winner)') == \ + n('American Prometheus: The Triumph and Tragedy of J. Robert Oppenheimer') + + +def test_parenthetical_and_subtitle_combined(): + assert n('Behold the Dreamers') == n("Behold the Dreamers (Oprah's Book Club): A Novel") + + +def test_trailing_edition_descriptor_is_dropped(): + assert n('10% Happier 10th Anniversary') == n('10% Happier') + assert n('Dune Deluxe Edition') == n('Dune') + + +def test_ampersand_equals_and(): + assert n('Crime & Punishment') == n('Crime and Punishment') + + +def test_distinct_titles_do_not_collide(): + assert n('The Hobbit') != n('The Silmarillion') + assert n('Dune') != n('Dune Messiah') + assert n('Be Useful') != n('Be Quiet') + + +# --- LibraryMatcher: exact-after-normalization ------------------------------- + +LIBRARY = [ + "All the King's Men", + 'Born a Crime: Stories from a South African Childhood', + 'Be Useful: Seven Tools for Life', + '10% Happier', + 'American Prometheus: The Triumph and Tragedy of J. Robert Oppenheimer', + 'The Hobbit', + 'The Lord of the Rings', +] + + +def test_matcher_finds_normalized_duplicates(): + m = LibraryMatcher(LIBRARY) + assert m.contains('All The King' + CURLY + 's Men') + assert m.contains('Born a Crime') + assert m.contains('Be Useful') + assert m.contains('10% Happier 10th Anniversary') + assert m.contains('American Prometheus (Pulitzer Prize Winner)') + + +def test_matcher_rejects_book_not_in_library(): + m = LibraryMatcher(LIBRARY) + assert not m.contains('A Wizard of Earthsea') + assert not m.contains('Assassin' + CURLY + 's Apprentice') + + +# --- LibraryMatcher: bounded fuzzy fallback ---------------------------------- + +def test_fuzzy_catches_minor_variation(): + # Fuzzy now runs only within a matching author. + m = LibraryMatcher([('The Lord of the Rings', 'J.R.R. Tolkien')]) + assert m.contains('The Lord of the Ring', 'J.R.R. Tolkien') # singular typo + assert m.contains('The Lord of teh Rings', 'J.R.R. Tolkien') # transposition + + +def test_fuzzy_disabled_for_short_titles(): + m = LibraryMatcher([('Dune', 'Frank Herbert')]) + assert not m.contains('June', 'Frank Herbert') # 1 edit away, too short to fuzzy + + +def test_fuzzy_threshold_rejects_different_book(): + m = LibraryMatcher([('The Hobbit', 'J.R.R. Tolkien')]) + assert not m.contains('The Rabbit', 'J.R.R. Tolkien') # similar but different book + + +def test_fuzzy_requires_matching_author(): + m = LibraryMatcher([('The Lord of the Rings', 'J.R.R. Tolkien')]) + assert not m.contains('The Lord of the Ring', 'Somebody Else') # author gate + + +# --- author-first behaviour -------------------------------------------------- + +def test_no_author_falls_back_to_exact_title_only(): + m = LibraryMatcher([('The Lord of the Rings', 'J.R.R. Tolkien')]) + assert m.contains('The Lord of the Rings') # exact title, no author -> ok + assert not m.contains('The Lord of the Ring') # looser matching needs an author + + +def test_author_subset_still_matches(): + # Kobo lists only one of the two co-authors; calibre lists both. + m = LibraryMatcher([('Good Omens', 'Terry Pratchett & Neil Gaiman')]) + assert m.contains('Good Omens', 'Neil Gaiman') + + +def test_same_title_different_author_is_not_a_match(): + # Title collision across authors must NOT match (the precision win). + m = LibraryMatcher([('Twilight', 'Stephenie Meyer')]) + assert not m.contains('Twilight', 'William Gay') + + +# --- real-world author drift (from Erez's library) --------------------------- + +def test_isbn_stored_as_second_author_is_ignored(): + # calibre stored an ISBN as a 2nd "author"; Kobo adds the illustrator. + m = LibraryMatcher([('Chloe and Cracker', 'Kelly McKain & 9781847153395')]) + assert m.contains('Chloe and Cracker', 'Kelly McKain, Mandy Stanley') + + +def test_honorific_vs_ordination_name_shares_surname(): + m = LibraryMatcher([('Mindfulness in Plain English', 'Henepola Gunaratana')]) + assert m.contains('Mindfulness in Plain English', 'Bhante Gunaratana') + + +def test_abbreviated_middle_name_and_subtitle(): + m = LibraryMatcher([('Say What You Mean: A Mindful Approach to Nonviolent ' + 'Communication', 'Oren J. Sofer')]) + assert m.contains('Say What You Mean', 'Oren Jay Sofer') + + +def test_shared_first_name_alone_does_not_match_different_titles(): + # Sharing only a first name is allowed as an author candidate, but the title + # gate still prevents merging genuinely different books. + m = LibraryMatcher([('The Firm', 'John Grisham')]) + assert not m.contains('A Perfect Spy', 'John le Carre') + + +def test_match_returns_the_matched_title(): + m = LibraryMatcher(['Born a Crime: Stories from a South African Childhood']) + assert m.match('Born a Crime') == normalize_title('Born a Crime') + assert m.match('Something Else Entirely') is None + + +# --- normalize_author -------------------------------------------------------- + +def test_author_order_and_format_are_ignored(): + # Flipped order and "Last, First" both reduce to the same key. + assert normalize_author('Grisham, John') == normalize_author('John Grisham') + assert normalize_author('Robert Penn Warren') == normalize_author('Warren, Robert Penn') + + +def test_distinct_authors_differ(): + assert normalize_author('John Grisham') != normalize_author('Stephen King') + + +# --- LibraryMatcher: same-author, article-insensitive match ------------------ + +def test_leading_article_match_when_author_agrees(): + m = LibraryMatcher([('The Hobbit', 'J.R.R. Tolkien')]) + assert m.contains('Hobbit', 'J.R.R. Tolkien') # Kobo dropped "The" + assert m.contains('Hobbit', 'Tolkien, J.R.R.') # + flipped author + + +def test_trailing_article_sort_form_matches(): + # calibre stores the sort form "..., A"; Kobo has the natural "A ...". + m = LibraryMatcher([('Guide for the Good Life, A', 'William Irvine')]) + assert m.contains('A Guide for the Good Life', 'William Irvine') + # ...and the reverse direction. + m2 = LibraryMatcher([('A Guide for the Good Life', 'Irvine, William')]) + assert m2.contains('Guide for the Good Life, A', 'William Irvine') + + +def test_article_match_requires_matching_author(): + m = LibraryMatcher([('The Hobbit', 'J.R.R. Tolkien')]) + assert not m.contains('Hobbit', 'Someone Else') # author gate blocks it + assert not m.contains('Hobbit') # no author -> no gate, no match + + +def test_article_match_does_not_merge_different_books_same_author(): + m = LibraryMatcher([('The Stand', 'Stephen King')]) + assert not m.contains('It', 'Stephen King') # same author, different book + + +# --- multi-author + colon-vs-no-colon (the New Yorker case) ------------------ + +def test_multi_author_order_and_separator_ignored(): + assert normalize_author('David Remnick & Bob Mankoff') == \ + normalize_author('Bob Mankoff, David Remnick') + + +def test_colon_in_one_title_only_still_matches(): + # Kobo writes the full title with a colon; calibre without one. + m = LibraryMatcher([('The New Yorker Encyclopedia of Cartoons', + 'Bob Mankoff, David Remnick')]) + assert m.contains('The New Yorker: Encyclopedia of Cartoons', + 'David Remnick & Bob Mankoff') + # ...and the reverse (colon on the calibre side). + m2 = LibraryMatcher([('The New Yorker: Encyclopedia of Cartoons', + 'Bob Mankoff & David Remnick')]) + assert m2.contains('The New Yorker Encyclopedia of Cartoons', + 'David Remnick, Bob Mankoff') + + +def test_colon_subtitle_only_on_one_side_still_matches_bare_title(): + # The original "Born a Crime" behaviour must still hold (stripped-form match). + m = LibraryMatcher(['Born a Crime: Stories from a South African Childhood']) + assert m.contains('Born a Crime')