Ver Fonte

Add duplicate book detection during import

- Detect re-import of same file by SHA-256 hash lookup before import
- Track new vs duplicate books separately during batch import
- Show appropriate toast messages for import results:
  - "Added N books" for all new imports
  - "N books already in library" for all duplicates
  - "Added N new, M duplicates" for mixed results
- Duplicates are silently updated (refreshes metadata if changed)

This helps users understand what happened when importing files
that may already exist in their library.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bernardo Magri há 1 mês atrás
pai
commit
8df4c43cda
2 ficheiros alterados com 49 adições e 20 exclusões
  1. 12 5
      PLAN.md
  2. 37 15
      src/BibliothecaWindow.cpp

+ 12 - 5
PLAN.md

@@ -125,12 +125,19 @@ Implemented:
 ## Phase 3: New Features
 
 ### 3.1 Duplicate Book Detection
-**Files**: `src/BookList.cpp`, `src/BibliothecaWindow.cpp`
-**Status**: Not Started
+**Files**: `src/BibliothecaWindow.cpp`
+**Status**: COMPLETED
 
-- [ ] Detect re-import of same file (by hash)
-- [ ] Prompt user: update existing or skip
-- [ ] Show notification for already-imported books
+Implemented:
+- [x] Detect re-import of same file by SHA-256 hash lookup
+- [x] Track new vs duplicate books during import
+- [x] Show appropriate toast messages:
+  - "Added N books" for all new
+  - "N books already in library" for all duplicates
+  - "Added N new, M duplicates" for mixed
+- [x] Duplicates are silently updated (metadata refresh)
+
+Note: Skipped prompt dialog - auto-update is simpler and non-destructive
 
 ### 3.2 Batch Operations
 **Files**: New files required

+ 37 - 15
src/BibliothecaWindow.cpp

@@ -358,11 +358,13 @@ void BibliothecaWindow::onAddBookClicked() {
       }
       if (files.empty()) return;
 
-      const size_t total_files = files.size();
-
       // Hash and create Book objects off the GTK thread
-      std::thread([this, files = std::move(files), total_files]() {
-        std::vector<Book> imported;
+      std::thread([this, files = std::move(files)]() {
+        struct ImportedBook {
+          Book book;
+          bool is_duplicate;
+        };
+        std::vector<ImportedBook> imported;
         std::vector<std::string> errors;
         imported.reserve(files.size());
 
@@ -370,7 +372,11 @@ void BibliothecaWindow::onAddBookClicked() {
           try {
             const auto path = f->get_path();
             const auto id = sha256_file(path);
-            auto resOpt   = import_book_assets(path, id);  // may throw -> catch already present
+
+            // Check if this book already exists (by hash)
+            bool is_duplicate = m_bookList.findById(id).has_value();
+
+            auto resOpt = import_book_assets(path, id);  // may throw -> catch already present
             ImportResult res = resOpt.value_or(ImportResult{});
 
             // Build the Book with extracted metadata
@@ -384,7 +390,7 @@ void BibliothecaWindow::onAddBookClicked() {
             // Preload cover into texture (or defer to BookTile which already calls load_cover_from_disk)
             b.load_cover_from_disk();
 
-            imported.push_back(std::move(b));
+            imported.push_back({std::move(b), is_duplicate});
           } catch (const std::exception& e) {
             errors.push_back(Glib::path_get_basename(f->get_path()) + ": " + e.what());
             std::cerr << "Import failed: " << e.what() << "\n";
@@ -392,22 +398,38 @@ void BibliothecaWindow::onAddBookClicked() {
         }
 
         // Push to model on the GTK main loop
-        Glib::signal_idle().connect_once([this, books = std::move(imported), errors = std::move(errors), total_files]() mutable {
-          if (!books.empty()) {
-            m_bookList.upsertMany(books);
+        Glib::signal_idle().connect_once([this, imported = std::move(imported), errors = std::move(errors)]() mutable {
+          std::vector<Book> books_to_add;
+          size_t new_count = 0;
+          size_t duplicate_count = 0;
+
+          for (auto& item : imported) {
+            if (item.is_duplicate) {
+              duplicate_count++;
+            } else {
+              new_count++;
+            }
+            books_to_add.push_back(std::move(item.book));
+          }
+
+          if (!books_to_add.empty()) {
+            m_bookList.upsertMany(books_to_add);
             updateVisibleView();
           }
 
           // Show toast with import results
-          const size_t success_count = books.size();
           const size_t error_count = errors.size();
 
-          if (error_count == 0 && success_count > 0) {
-            showToast(Glib::ustring::compose("Added %1 book%2", success_count, success_count == 1 ? "" : "s"), false);
-          } else if (success_count == 0 && error_count > 0) {
+          if (error_count == 0 && new_count > 0 && duplicate_count == 0) {
+            showToast(Glib::ustring::compose("Added %1 book%2", new_count, new_count == 1 ? "" : "s"), false);
+          } else if (error_count == 0 && new_count == 0 && duplicate_count > 0) {
+            showToast(Glib::ustring::compose("%1 book%2 already in library", duplicate_count, duplicate_count == 1 ? "" : "s"), false);
+          } else if (error_count == 0 && new_count > 0 && duplicate_count > 0) {
+            showToast(Glib::ustring::compose("Added %1 new, %2 duplicate%3", new_count, duplicate_count, duplicate_count == 1 ? "" : "s"), false);
+          } else if (new_count == 0 && duplicate_count == 0 && error_count > 0) {
             showToast(Glib::ustring::compose("Failed to import %1 file%2", error_count, error_count == 1 ? "" : "s"), true);
-          } else if (success_count > 0 && error_count > 0) {
-            showToast(Glib::ustring::compose("Added %1, failed %2", success_count, error_count), true);
+          } else if ((new_count > 0 || duplicate_count > 0) && error_count > 0) {
+            showToast(Glib::ustring::compose("Added %1, %2 failed", new_count + duplicate_count, error_count), true);
           }
         });
       }).detach();