fix: three audio scan bugs — signal shadow, re-entrancy, S-key jump

1. Rename ScanWorker.finished → scan_done to stop shadowing
   QThread.finished. Previously, cancelled scans leaked the QThread
   because the custom signal was never emitted.

2. Block signals on combobox reset in _on_scan_ref_changed to
   prevent re-entrant call when user cancels folder dialog.

3. Merge overlapping scan regions into clusters before S-key
   navigation so it jumps to the next distinct match, not 1s forward
   through overlapping windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 09:12:24 +02:00
parent 1e2cebd424
commit 73d5367424
+18 -8
View File
@@ -187,7 +187,7 @@ class FrameGrabber(QThread):
class ScanWorker(QThread): class ScanWorker(QThread):
"""Runs audio similarity scan off the main thread.""" """Runs audio similarity scan off the main thread."""
finished = pyqtSignal(list) # emits list of (start, end, score) scan_done = pyqtSignal(list) # emits list of (start, end, score)
error = pyqtSignal(str) error = pyqtSignal(str)
progress = pyqtSignal(str) # status message progress = pyqtSignal(str) # status message
@@ -220,7 +220,7 @@ class ScanWorker(QThread):
cancel_flag=self, cancel_flag=self,
) )
if not self._cancel: if not self._cancel:
self.finished.emit(regions) self.scan_done.emit(regions)
except Exception as e: except Exception as e:
if not self._cancel: if not self._cancel:
self.error.emit(str(e)) self.error.emit(str(e))
@@ -2508,19 +2508,21 @@ class MainWindow(QMainWindow):
if folder: if folder:
self._scan_folder = folder self._scan_folder = folder
else: else:
self._cmb_scan_ref.blockSignals(True)
self._cmb_scan_ref.setCurrentIndex(0) self._cmb_scan_ref.setCurrentIndex(0)
self._cmb_scan_ref.blockSignals(False)
def _cleanup_scan_worker(self) -> None: def _cleanup_scan_worker(self) -> None:
"""Disconnect signals and schedule deletion of old scan worker.""" """Disconnect signals and schedule deletion of old scan worker."""
if self._scan_worker is not None: if self._scan_worker is not None:
try: try:
self._scan_worker.finished.disconnect() self._scan_worker.scan_done.disconnect()
self._scan_worker.error.disconnect() self._scan_worker.error.disconnect()
self._scan_worker.progress.disconnect() self._scan_worker.progress.disconnect()
except TypeError: except TypeError:
pass # already disconnected pass # already disconnected
if self._scan_worker.isRunning(): if self._scan_worker.isRunning():
# Let the thread finish naturally; deleteLater when done # QThread.finished fires when run() returns, even on cancel
self._scan_worker.finished.connect(self._scan_worker.deleteLater) self._scan_worker.finished.connect(self._scan_worker.deleteLater)
else: else:
self._scan_worker.deleteLater() self._scan_worker.deleteLater()
@@ -2566,7 +2568,7 @@ class MainWindow(QMainWindow):
self._show_status(f"Scanning with {len(clip_paths)} reference clips...") self._show_status(f"Scanning with {len(clip_paths)} reference clips...")
self._scan_worker = ScanWorker(self._file_path, clip_paths, mode, threshold) self._scan_worker = ScanWorker(self._file_path, clip_paths, mode, threshold)
self._scan_worker.finished.connect(self._on_scan_done) self._scan_worker.scan_done.connect(self._on_scan_done)
self._scan_worker.error.connect(self._on_scan_error) self._scan_worker.error.connect(self._on_scan_error)
self._scan_worker.progress.connect(self._show_status) self._scan_worker.progress.connect(self._show_status)
self._scan_worker.start() self._scan_worker.start()
@@ -2587,12 +2589,20 @@ class MainWindow(QMainWindow):
regions = sorted(self._timeline._scan_regions, key=lambda r: r[0]) regions = sorted(self._timeline._scan_regions, key=lambda r: r[0])
if not regions: if not regions:
return return
for (start, _end, _score) in regions: # Merge overlapping regions into clusters so S jumps past each group
clusters: list[tuple[float, float]] = []
for (start, end, _score) in regions:
if clusters and start <= clusters[-1][1]:
clusters[-1] = (clusters[-1][0], max(clusters[-1][1], end))
else:
clusters.append((start, end))
# Jump to the start of the next cluster after cursor
for (start, _end) in clusters:
if start > self._cursor + 0.1: if start > self._cursor + 0.1:
self._step_cursor(start - self._cursor) self._step_cursor(start - self._cursor)
return return
# Wrap to first region # Wrap to first cluster
self._step_cursor(regions[0][0] - self._cursor) self._step_cursor(clusters[0][0] - self._cursor)
# --- Export --- # --- Export ---