66# See CONTRIBUTORS for full list of authors.
77
88import sys
9+ from datetime import datetime
910from sqlalchemy import func
1011from flask_babel import lazy_gettext as N_
1112
1213from cps import calibre_db , db , logger
14+ from cps .duplicates import find_duplicate_books_python , find_duplicate_candidate_ids_sql
1315from cps .services .worker import CalibreTask , STAT_CANCELLED , STAT_ENDED
1416from cps .ub import init_db_thread
1517
@@ -54,9 +56,11 @@ def run(self, worker_thread):
5456 cwa_db = CWA_DB ()
5557 cache_data = cwa_db .get_duplicate_cache () or {}
5658
57- if not self .full_scan and not cache_data .get ('scan_pending' , True ):
58- self ._handleSuccess ()
59- return
59+ if not self .full_scan :
60+ last_scanned_book_id = int (cache_data .get ('last_scanned_book_id' ) or 0 )
61+ if last_scanned_book_id == 0 :
62+ # No baseline yet; fall back to full scan
63+ self .full_scan = True
6064
6165 self .progress = 0.1
6266 self .message = N_ ('Scanning for duplicates' )
@@ -67,26 +71,120 @@ def run(self, worker_thread):
6771 if self .stat in (STAT_CANCELLED , STAT_ENDED ):
6872 return
6973
70- duplicate_groups = find_duplicate_books (include_dismissed = False , user_id = self .user_id )
71- self .result_count = len (duplicate_groups )
72-
73- if self .stat in (STAT_CANCELLED , STAT_ENDED ):
74- return
75-
76- # Update cache with full results (including dismissed groups)
77- all_groups = find_duplicate_books (include_dismissed = True , user_id = self .user_id )
78-
79- max_book_id = 0
80- try :
81- max_id_result = calibre_db .session .query (func .max (db .Books .id )).scalar ()
82- max_book_id = max_id_result if max_id_result is not None else 0
83- except Exception as ex :
84- log .warning ("[cwa-duplicates] Could not get max book ID in TaskDuplicateScan: %s" , str (ex ))
85-
86- cwa_db .update_duplicate_cache (all_groups , len (all_groups ), max_book_id )
74+ if self .full_scan :
75+ duplicate_groups = find_duplicate_books (include_dismissed = False , user_id = self .user_id )
76+ self .result_count = len (duplicate_groups )
77+
78+ if self .stat in (STAT_CANCELLED , STAT_ENDED ):
79+ return
80+
81+ # Update cache with full results (including dismissed groups)
82+ all_groups = find_duplicate_books (include_dismissed = True , user_id = self .user_id )
83+
84+ max_book_id = 0
85+ try :
86+ max_id_result = calibre_db .session .query (func .max (db .Books .id )).scalar ()
87+ max_book_id = max_id_result if max_id_result is not None else 0
88+ except Exception as ex :
89+ log .warning ("[cwa-duplicates] Could not get max book ID in TaskDuplicateScan: %s" , str (ex ))
90+
91+ cwa_db .update_duplicate_cache (all_groups , len (all_groups ), max_book_id )
92+ log .info ("[cwa-duplicates] Duplicate cache updated (full scan): groups=%s max_book_id=%s" ,
93+ len (all_groups ), max_book_id )
94+ else :
95+ # Incremental scan: only groups impacted by newly added books
96+ settings = cwa_db .cwa_settings
97+ use_title = settings .get ('duplicate_detection_title' , 1 )
98+ use_author = settings .get ('duplicate_detection_author' , 1 )
99+ use_language = settings .get ('duplicate_detection_language' , 1 )
100+ use_series = settings .get ('duplicate_detection_series' , 0 )
101+ use_publisher = settings .get ('duplicate_detection_publisher' , 0 )
102+ use_format = settings .get ('duplicate_detection_format' , 0 )
103+
104+ last_scanned_book_id = int (cache_data .get ('last_scanned_book_id' ) or 0 )
105+ candidate_ids = find_duplicate_candidate_ids_sql (use_title , use_author , user_id = self .user_id ,
106+ min_book_id = last_scanned_book_id )
107+
108+ max_book_id = 0
109+ try :
110+ max_id_result = calibre_db .session .query (func .max (db .Books .id )).scalar ()
111+ max_book_id = max_id_result if max_id_result is not None else 0
112+ except Exception as ex :
113+ log .warning ("[cwa-duplicates] Could not get max book ID in TaskDuplicateScan: %s" , str (ex ))
114+
115+ if not candidate_ids :
116+ # No impacted groups; just bump last_scanned_book_id
117+ try :
118+ cwa_db .cur .execute ("""
119+ UPDATE cwa_duplicate_cache
120+ SET last_scanned_book_id = ?, scan_pending = 0, scan_timestamp = ?
121+ WHERE id = 1
122+ """ , (max_book_id , datetime .now ().isoformat ()))
123+ cwa_db .con .commit ()
124+ log .info ("[cwa-duplicates] Incremental scan: no candidates; cache timestamp updated (last_scanned_book_id=%s)" ,
125+ max_book_id )
126+ except Exception :
127+ pass
128+ self .result_count = 0
129+ else :
130+ # Identify affected groups in cache
131+ cached_groups = cache_data .get ('duplicate_groups' , [])
132+ candidate_set = set (candidate_ids )
133+ affected_hashes = {
134+ group .get ('group_hash' )
135+ for group in cached_groups
136+ if group .get ('book_ids' ) and candidate_set .intersection (set (group .get ('book_ids' )))
137+ }
138+
139+ # Recompute groups for candidate IDs (include dismissed for cache)
140+ recomputed_groups = find_duplicate_books_python (
141+ use_title , use_author , use_language , use_series , use_publisher , use_format ,
142+ include_dismissed = True , user_id = self .user_id , candidate_ids = candidate_ids
143+ )
144+
145+ # Serialize recomputed groups
146+ serialized_groups = []
147+ for group in recomputed_groups :
148+ serialized_groups .append ({
149+ 'title' : group .get ('title' , '' ),
150+ 'author' : group .get ('author' , '' ),
151+ 'count' : group .get ('count' , 0 ),
152+ 'group_hash' : group .get ('group_hash' , '' ),
153+ 'book_ids' : [book .id for book in group .get ('books' , [])]
154+ })
155+
156+ # Merge cache: remove affected, then append recomputed
157+ kept_groups = [g for g in cached_groups if g .get ('group_hash' ) not in affected_hashes ]
158+ merged_groups = kept_groups + serialized_groups
159+
160+ try :
161+ import json
162+ cwa_db .cur .execute ("""
163+ UPDATE cwa_duplicate_cache
164+ SET scan_timestamp = ?,
165+ duplicate_groups_json = ?,
166+ total_count = ?,
167+ scan_pending = 0,
168+ last_scanned_book_id = ?
169+ WHERE id = 1
170+ """ , (datetime .now ().isoformat (), json .dumps (merged_groups ), len (merged_groups ), max_book_id ))
171+ cwa_db .con .commit ()
172+ log .info ("[cwa-duplicates] Duplicate cache updated (incremental): merged_groups=%s max_book_id=%s" ,
173+ len (merged_groups ), max_book_id )
174+ except Exception as ex :
175+ log .warning ("[cwa-duplicates] Failed to update incremental cache: %s" , str (ex ))
176+
177+ # Result count is unresolved duplicates for this run (exclude dismissed)
178+ self .result_count = len (find_duplicate_books_python (
179+ use_title , use_author , use_language , use_series , use_publisher , use_format ,
180+ include_dismissed = False , user_id = self .user_id , candidate_ids = candidate_ids
181+ ))
87182
88183 self .progress = 1
89- self .message = N_ ('Duplicate scan completed: %(count)s groups' , count = len (duplicate_groups ))
184+ if self .full_scan :
185+ self .message = N_ ('Duplicate scan completed: %(count)s groups' , count = self .result_count )
186+ else :
187+ self .message = N_ ('Duplicate scan completed: %(count)s new groups' , count = self .result_count )
90188 self ._handleSuccess ()
91189 except Exception as ex :
92190 log .error ("[cwa-duplicates] Duplicate scan task failed: %s" , str (ex ))
0 commit comments