Fix UI freeze on iPod mount/track-load and add content_hash/sync-metadata
- Move mount_device, get_device_info, and track scanning to background WorkerThread (new 'mount_and_load' task type) so the UI stays responsive - Fix DeviceMonitor auto-poll to run detect_devices() in a background thread - Refactor _on_device_detection_finished and _on_mount_clicked to use the new worker; remove synchronous _set_device_mounted - Avoid double scan (get_track_count + get_all_tracks) — scan once in worker - Add content_hash fingerprint to _scan_ipod_files and library_cache for track matching - Add update_track_metadata / _write_databases_from_tracks with artwork override support in Nano7Database - Add 'Sync Metadata' button and context menu action in LibraryTab
This commit is contained in:
+60
-51
@@ -132,21 +132,9 @@ class iPodTab(QWidget):
|
||||
|
||||
if self.ipod_devices:
|
||||
device = self.ipod_devices[0]
|
||||
device_id = device["id"]
|
||||
detected_mount = device.get("mount_point")
|
||||
already_mounted = device.get("mounted", False)
|
||||
|
||||
if already_mounted and detected_mount:
|
||||
self._set_device_mounted(device, detected_mount)
|
||||
else:
|
||||
ipod = IPodDevice()
|
||||
mount_point = ipod.mount_device(device_id, detected_mount=detected_mount)
|
||||
if mount_point:
|
||||
device["mount_point"] = mount_point
|
||||
device["mounted"] = True
|
||||
self._set_device_mounted(device, mount_point)
|
||||
else:
|
||||
self._set_device_unmounted(device)
|
||||
self._start_mount_and_load(device, detected_mount, already_mounted)
|
||||
else:
|
||||
self._set_device_not_found()
|
||||
else:
|
||||
@@ -154,27 +142,67 @@ class iPodTab(QWidget):
|
||||
|
||||
self._cleanup_worker()
|
||||
|
||||
def _set_device_mounted(self, device: dict, mount_point: str):
|
||||
self.current_mount_point = mount_point
|
||||
ipod = IPodDevice(mount_point=mount_point)
|
||||
device_info = ipod.get_device_info()
|
||||
device["info"] = device_info
|
||||
|
||||
free_space = device_info.get("free_space", 0)
|
||||
total_space = device_info.get("total_space", 0)
|
||||
|
||||
self.device_info_label.setText(
|
||||
f"Device: {device['name']}\n"
|
||||
f"Mount Point: {mount_point}\n"
|
||||
f"Free Space: {free_space / 1024**2:.1f} MB / {total_space / 1024**2:.1f} MB"
|
||||
)
|
||||
self.device_status_label.setText("Status: Mounted and ready")
|
||||
def _start_mount_and_load(self, device: dict, mount_point=None, already_mounted=False):
|
||||
self.device_status_label.setText("Mounting and loading tracks...")
|
||||
self.mount_button.setEnabled(False)
|
||||
self.eject_button.setEnabled(True)
|
||||
|
||||
self.device_mounted.emit(mount_point)
|
||||
self.worker_thread = WorkerThread(
|
||||
task_type="mount_and_load",
|
||||
device=device,
|
||||
mount_point=mount_point,
|
||||
already_mounted=already_mounted,
|
||||
)
|
||||
self.worker_thread.progress_signal.connect(self._on_mount_load_progress)
|
||||
self.worker_thread.finished_signal.connect(self._on_mount_load_finished)
|
||||
self.worker_thread.start()
|
||||
|
||||
self._load_ipod_tracks()
|
||||
def _on_mount_load_progress(self, progress, status):
|
||||
self.device_info_label.setText(status)
|
||||
|
||||
def _on_mount_load_finished(self, success, message, result):
|
||||
if success:
|
||||
device = result["device"]
|
||||
mount_point = result["mount_point"]
|
||||
device_info = result["device_info"]
|
||||
tracks = result["tracks"]
|
||||
|
||||
self.current_mount_point = mount_point
|
||||
device["info"] = device_info
|
||||
|
||||
free_space = device_info.get("free_space", 0)
|
||||
total_space = device_info.get("total_space", 0)
|
||||
|
||||
self.device_info_label.setText(
|
||||
f"Device: {device['name']}\n"
|
||||
f"Mount Point: {mount_point}\n"
|
||||
f"Free Space: {free_space / 1024**2:.1f} MB / {total_space / 1024**2:.1f} MB"
|
||||
)
|
||||
self.device_status_label.setText("Status: Mounted and ready")
|
||||
self.mount_button.setEnabled(False)
|
||||
self.eject_button.setEnabled(True)
|
||||
|
||||
self.device_mounted.emit(mount_point)
|
||||
|
||||
self.transferred_list.clear()
|
||||
for track in tracks:
|
||||
display = f"{track['artist']} — {track['title']}"
|
||||
if track.get("album"):
|
||||
display += f" ({track['album']})"
|
||||
item = QListWidgetItem(display)
|
||||
item.setData(Qt.ItemDataRole.UserRole, track)
|
||||
self.transferred_list.addItem(item)
|
||||
|
||||
self.track_count_label.setText(f"Tracks on device: {len(tracks)}")
|
||||
self.export_button.setEnabled(len(tracks) > 0)
|
||||
else:
|
||||
self.device_status_label.setText(f"Status: {message}")
|
||||
self.mount_button.setEnabled(True)
|
||||
if self.ipod_devices:
|
||||
self._set_device_unmounted(self.ipod_devices[0])
|
||||
else:
|
||||
self._set_device_not_found()
|
||||
|
||||
self._cleanup_worker()
|
||||
|
||||
def _set_device_unmounted(self, device: dict):
|
||||
self.current_mount_point = None
|
||||
@@ -213,27 +241,8 @@ class iPodTab(QWidget):
|
||||
return
|
||||
|
||||
device = self.ipod_devices[0]
|
||||
device_id = device["id"]
|
||||
detected_mount = device.get("mount_point")
|
||||
|
||||
self.device_status_label.setText("Mounting...")
|
||||
self.mount_button.setEnabled(False)
|
||||
|
||||
try:
|
||||
ipod = IPodDevice()
|
||||
mount_point = ipod.mount_device(device_id, detected_mount=detected_mount)
|
||||
if mount_point:
|
||||
device["mount_point"] = mount_point
|
||||
device["mounted"] = True
|
||||
self._set_device_mounted(device, mount_point)
|
||||
else:
|
||||
self.device_status_label.setText("Status: Mount failed")
|
||||
self.mount_button.setEnabled(True)
|
||||
QMessageBox.warning(self, "Mount Error", "Failed to mount iPod. Check permissions and try again.")
|
||||
except Exception as e:
|
||||
self.device_status_label.setText("Status: Mount error")
|
||||
self.mount_button.setEnabled(True)
|
||||
QMessageBox.warning(self, "Mount Error", f"Error mounting iPod: {e}")
|
||||
self._start_mount_and_load(device, detected_mount, already_mounted=False)
|
||||
|
||||
def _on_eject_clicked(self):
|
||||
reply = QMessageBox.question(
|
||||
|
||||
+91
-1
@@ -214,6 +214,12 @@ class LibraryTab(QWidget):
|
||||
self.library_transfer_btn.clicked.connect(self._on_library_transfer_clicked)
|
||||
toolbar.addWidget(self.library_transfer_btn)
|
||||
|
||||
self.library_sync_btn = QPushButton("\uD83D\uDD04 Sync Metadata")
|
||||
self.library_sync_btn.setEnabled(False)
|
||||
self.library_sync_btn.setToolTip("Sync metadata and artwork for all tracks on iPod")
|
||||
self.library_sync_btn.clicked.connect(self._on_sync_metadata_clicked)
|
||||
toolbar.addWidget(self.library_sync_btn)
|
||||
|
||||
self.library_remove_btn = QPushButton("\uD83D\uDDD1\uFE0F Remove")
|
||||
self.library_remove_btn.setEnabled(False)
|
||||
self.library_remove_btn.clicked.connect(self._on_library_remove_selected)
|
||||
@@ -233,6 +239,7 @@ class LibraryTab(QWidget):
|
||||
self.library_add_btn,
|
||||
self.library_convert_btn,
|
||||
self.library_transfer_btn,
|
||||
self.library_sync_btn,
|
||||
self.library_remove_btn,
|
||||
self.library_edit_btn,
|
||||
refresh_btn,
|
||||
@@ -717,7 +724,9 @@ class LibraryTab(QWidget):
|
||||
has_ready = ready_count > 0
|
||||
has_source = source_count > 0
|
||||
|
||||
self.library_transfer_btn.setEnabled(has_ready and self._current_mount_point is not None)
|
||||
has_mount = self._current_mount_point is not None
|
||||
self.library_transfer_btn.setEnabled(has_ready and has_mount)
|
||||
self.library_sync_btn.setEnabled(has_ready and has_mount)
|
||||
self.library_convert_btn.setEnabled(has_source)
|
||||
self.library_remove_btn.setEnabled(len(all_tracks) > 0)
|
||||
self.library_edit_btn.setEnabled(len(all_tracks) > 0)
|
||||
@@ -984,6 +993,8 @@ class LibraryTab(QWidget):
|
||||
play_action.triggered.connect(lambda: self._play_track_from_index(row))
|
||||
|
||||
if is_ready and self._current_mount_point is not None:
|
||||
sync_action = menu.addAction("\uD83D\uDD04 Sync Metadata to iPod")
|
||||
sync_action.triggered.connect(self._on_sync_metadata_clicked)
|
||||
transfer_action = menu.addAction("\uD83D\uDCE4 Transfer to iPod")
|
||||
transfer_action.triggered.connect(self._on_library_transfer_clicked)
|
||||
else:
|
||||
@@ -1129,6 +1140,84 @@ class LibraryTab(QWidget):
|
||||
|
||||
self._cleanup_worker()
|
||||
|
||||
def _on_sync_metadata_clicked(self):
|
||||
selected_tracks = self._get_selected_ready_tracks()
|
||||
if not selected_tracks:
|
||||
QMessageBox.warning(self, "Error", "No tracks selected.")
|
||||
return
|
||||
|
||||
if not self._current_mount_point:
|
||||
QMessageBox.warning(self, "Error", "No iPod device mounted.")
|
||||
return
|
||||
|
||||
if self.worker_thread and self.worker_thread.isRunning():
|
||||
QMessageBox.information(self, "Info", "Please wait for the current task to finish")
|
||||
return
|
||||
|
||||
self.library_sync_btn.setEnabled(False)
|
||||
self.library_status.setText("Matching tracks on iPod...")
|
||||
self.library_progress.setValue(0)
|
||||
|
||||
from ipod_nano7_db import Nano7Database, content_hash as compute_content_hash
|
||||
|
||||
db = Nano7Database(self._current_mount_point)
|
||||
ipod_tracks = db.get_all_tracks()
|
||||
ipod_by_hash = {t.get("content_hash", ""): t for t in ipod_tracks if t.get("content_hash")}
|
||||
|
||||
sync_batch = []
|
||||
errors = []
|
||||
for track_info, local_path in selected_tracks:
|
||||
ch = compute_content_hash(local_path)
|
||||
ipod_track = ipod_by_hash.get(ch)
|
||||
if ipod_track is None:
|
||||
ipod_track = db.find_track_by_metadata(
|
||||
track_info.title, track_info.artist, track_info.album or "",
|
||||
)
|
||||
if ipod_track is None:
|
||||
errors.append(f"'{track_info.title}' — not found on iPod")
|
||||
continue
|
||||
|
||||
sync_batch.append({
|
||||
"ipod_pid": ipod_track["pid"],
|
||||
"local_path": local_path,
|
||||
"new_metadata": {
|
||||
"title": track_info.title,
|
||||
"artist": track_info.artist,
|
||||
"album": track_info.album or "",
|
||||
"track_number": track_info.track_number or 0,
|
||||
"genre": track_info.genre or "",
|
||||
},
|
||||
})
|
||||
|
||||
if not sync_batch:
|
||||
msg = "No matching tracks found on iPod"
|
||||
if errors:
|
||||
msg += ":\n" + "\n".join(errors[:5])
|
||||
QMessageBox.warning(self, "Sync Error", msg)
|
||||
return
|
||||
|
||||
if errors:
|
||||
logger.warning("Sync metadata skipped for %d unmatched track(s)", len(errors))
|
||||
|
||||
self.worker_thread = WorkerThread(
|
||||
task_type="sync_metadata",
|
||||
sync_batch=sync_batch,
|
||||
mount_point=self._current_mount_point,
|
||||
)
|
||||
self.worker_thread.progress_signal.connect(self._on_library_progress)
|
||||
self.worker_thread.finished_signal.connect(self._on_sync_metadata_finished)
|
||||
self.worker_thread.start()
|
||||
|
||||
def _on_sync_metadata_finished(self, success, message, result):
|
||||
self.library_sync_btn.setEnabled(True)
|
||||
if success:
|
||||
self.library_status.setText(message)
|
||||
QMessageBox.information(self, "Sync Complete", message)
|
||||
else:
|
||||
self.library_status.setText(f"Error: {message}")
|
||||
QMessageBox.warning(self, "Sync Error", message)
|
||||
self._cleanup_worker()
|
||||
|
||||
def _cleanup_worker(self):
|
||||
if self.worker_thread:
|
||||
self.worker_thread.wait(3000)
|
||||
@@ -1168,6 +1257,7 @@ class LibraryTab(QWidget):
|
||||
def on_device_unmounted(self):
|
||||
self._current_mount_point = None
|
||||
self.library_transfer_btn.setEnabled(False)
|
||||
self.library_sync_btn.setEnabled(False)
|
||||
self.library_progress.setVisible(False)
|
||||
self.library_status.setVisible(False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user