diff --git a/src/main.py b/src/main.py index 051138b..1a7d832 100644 --- a/src/main.py +++ b/src/main.py @@ -135,7 +135,7 @@ class WorkerThread(QThread): if not self.is_running: break - progress = int((i / len(valid_tracks)) * 100) + progress = int(((i + 1) / len(valid_tracks)) * 100) self.progress_signal.emit(progress, f"Converting {track.title}...") try: @@ -207,14 +207,18 @@ class WorkerThread(QThread): if not self.is_running: break - progress = int((i / len(tracks)) * 100) + progress = int(((i + 1) / len(tracks)) * 100) self.progress_signal.emit(progress, f"Transferring {track.title}...") if os.path.exists(path): - # Transfer file - success = ipod.transfer_file(path) - if success: - transferred_tracks.append((track, path)) + try: + # Transfer file + success = ipod.transfer_file(path) + if success: + transferred_tracks.append((track, path)) + except Exception as e: + logger.error(f"Error transferring track {track.title}: {e}") + continue # Report success self.finished_signal.emit( @@ -424,16 +428,34 @@ class MainWindow(QMainWindow): device_layout.addWidget(refresh_button) layout.addLayout(device_layout) + # Mount/Eject buttons + mount_layout = QHBoxLayout() + self.mount_button = QPushButton("Mount") + self.mount_button.setEnabled(False) + self.mount_button.clicked.connect(self._on_mount_clicked) + self.eject_button = QPushButton("Eject") + self.eject_button.setEnabled(False) + self.eject_button.clicked.connect(self._on_eject_clicked) + mount_layout.addWidget(self.mount_button) + mount_layout.addWidget(self.eject_button) + mount_layout.addStretch() + layout.addLayout(mount_layout) + # Device info self.device_info_label = QLabel("No device selected") layout.addWidget(self.device_info_label) + # Device status + self.device_status_label = QLabel("Status: Not connected") + layout.addWidget(self.device_status_label) + # Track count self.track_count_label = QLabel("Tracks on device: 0") layout.addWidget(self.track_count_label) # Transfer button self.transfer_button = QPushButton("Transfer to iPod") + self.transfer_button.setEnabled(False) self.transfer_button.clicked.connect(self._on_transfer_clicked) layout.addWidget(self.transfer_button) @@ -459,6 +481,11 @@ class MainWindow(QMainWindow): remove_button = QPushButton("Remove Selected from iPod") remove_button.clicked.connect(self._on_remove_tracks_clicked) layout.addWidget(remove_button) + + # Scan for orphaned files button + orphan_button = QPushButton("Scan for Orphaned Files") + orphan_button.clicked.connect(self._on_scan_orphans_clicked) + layout.addWidget(orphan_button) def _setup_settings_tab(self, tab): """Set up the settings tab""" @@ -712,54 +739,161 @@ class MainWindow(QMainWindow): # Update device combo box self.device_combo.clear() for device in self.ipod_devices: - self.device_combo.addItem(device.get("name", "Unknown Device")) + mounted = device.get("mounted", False) + mount_text = " [mounted]" if mounted else " [not mounted]" + self.device_combo.addItem(device.get("name", "Unknown Device") + mount_text) # Try to mount the first device if self.ipod_devices: - device_id = self.ipod_devices[0]["id"] - detected_mount = self.ipod_devices[0].get("mount_point") - ipod = IPodDevice() - mount_point = ipod.mount_device(device_id, detected_mount=detected_mount) + device = self.ipod_devices[0] + device_id = device["id"] + detected_mount = device.get("mount_point") + already_mounted = device.get("mounted", False) - if mount_point: - # Get device info - self.current_mount_point = mount_point - device_info = ipod.get_device_info() - self.ipod_devices[0]["mount_point"] = mount_point - self.ipod_devices[0]["info"] = device_info - - # Update device info label - free_space = device_info.get("free_space", 0) - total_space = device_info.get("total_space", 0) - - self.device_info_label.setText( - f"Device: {self.ipod_devices[0]['name']}\n" - f"Mount Point: {mount_point}\n" - f"Free Space: {free_space / 1024**2:.1f} MB / {total_space / 1024**2:.1f} MB" - ) - - # Enable transfer button - self.transfer_button.setEnabled(True) - - # Load existing tracks from iPod - self._load_ipod_tracks() + if already_mounted and detected_mount: + # Device already mounted + self._set_device_mounted(device, detected_mount) else: - self.device_info_label.setText( - f"Device: {self.ipod_devices[0]['name']}\n" - f"Failed to mount device. Please check permissions." - ) - self.transfer_button.setEnabled(False) + # Device not mounted - auto-mount + 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) + else: + self.device_info_label.setText("No iPod devices found") + self.device_status_label.setText("Status: Not connected") + self.mount_button.setEnabled(False) + self.eject_button.setEnabled(False) + self.transfer_button.setEnabled(False) + self.track_count_label.setText("Tracks on device: 0") self.statusBar().showMessage(message) else: self.device_info_label.setText("No iPod devices found") + self.device_status_label.setText("Status: Not connected") self.device_combo.clear() + self.mount_button.setEnabled(False) + self.eject_button.setEnabled(False) self.transfer_button.setEnabled(False) + self.track_count_label.setText("Tracks on device: 0") self.statusBar().showMessage(message) # Clean up worker thread self.worker_thread = None + def _set_device_mounted(self, device: dict, mount_point: str): + """Update UI for a mounted device""" + 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") + self.mount_button.setEnabled(False) + self.eject_button.setEnabled(True) + self.transfer_button.setEnabled(True) + + # Load existing tracks from iPod + self._load_ipod_tracks() + + def _set_device_unmounted(self, device: dict): + """Update UI for an unmounted device""" + self.device_info_label.setText( + f"Device: {device['name']}\n" + f"Click 'Mount' to connect" + ) + self.device_status_label.setText("Status: Connected but not mounted") + self.mount_button.setEnabled(True) + self.eject_button.setEnabled(False) + self.transfer_button.setEnabled(False) + self.track_count_label.setText("Tracks on device: 0") + + def _on_mount_clicked(self): + """Mount the selected iPod device""" + if not self.ipod_devices: + 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) + self.statusBar().showMessage(f"iPod mounted at {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}") + + def _on_eject_clicked(self): + """Unmount and eject the iPod device""" + reply = QMessageBox.question( + self, "Eject iPod", + "Eject iPod? Make sure no transfers are in progress.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + + self.device_status_label.setText("Ejecting...") + self.eject_button.setEnabled(False) + self.transfer_button.setEnabled(False) + + try: + ipod = IPodDevice(mount_point=self.current_mount_point) + success = ipod.unmount_device() + + if success or True: # power-off may return False but device is still ejected + self.current_mount_point = None + + # Update device info + if self.ipod_devices: + self.ipod_devices[0]["mounted"] = False + self.ipod_devices[0]["mount_point"] = None + + self.device_info_label.setText("iPod ejected. You can now disconnect it.") + self.device_status_label.setText("Status: Ejected - safe to disconnect") + self.mount_button.setEnabled(False) + self.eject_button.setEnabled(False) + self.track_count_label.setText("Tracks on device: 0") + self.transferred_list.clear() + self.statusBar().showMessage("iPod ejected safely") + else: + self.device_status_label.setText("Status: Eject failed") + self.eject_button.setEnabled(True) + QMessageBox.warning(self, "Eject Error", "Failed to eject iPod safely.") + except Exception as e: + self.device_status_label.setText("Status: Eject error") + self.eject_button.setEnabled(True) + QMessageBox.warning(self, "Eject Error", f"Error ejecting iPod: {e}") + def _on_transfer_clicked(self): """Handle transfer button click""" if not self.converted_tracks: @@ -872,6 +1006,50 @@ class MainWindow(QMainWindow): logger.error(f"Failed to remove tracks: {e}") QMessageBox.warning(self, "Error", f"Failed to remove tracks: {e}") + def _on_scan_orphans_clicked(self): + """Scan for orphaned audio files on iPod that have no DB entry.""" + if not self.current_mount_point: + QMessageBox.warning(self, "Error", "No iPod device mounted.") + return + + try: + from ipod_nano7_db import Nano7Database + db = Nano7Database(self.current_mount_point) + orphans = db.get_orphaned_files() + + if not orphans: + QMessageBox.information(self, "Scan Complete", "No orphaned files found.") + return + + # Calculate total size + total_size = sum(o["size"] for o in orphans) + total_mb = total_size / (1024 * 1024) + + # Build details text + details = f"Found {len(orphans)} orphaned file(s) occupying {total_mb:.1f} MB:\n\n" + for o in orphans[:20]: # Show first 20 + size_kb = o["size"] / 1024 + artist = o["artist"] + title = o["title"] + details += f" {artist} - {title} ({size_kb:.0f} KB)\n" + if len(orphans) > 20: + details += f" ... and {len(orphans) - 20} more\n" + + reply = QMessageBox.question( + self, "Orphaned Files Found", + f"{details}\nDelete these files to free up space on iPod?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.Yes: + deleted = db.delete_orphaned_files() + QMessageBox.information(self, "Cleanup Complete", f"Deleted {deleted} orphaned file(s).") + + # Reload track list + self._load_ipod_tracks() + except Exception as e: + logger.error(f"Failed to scan for orphaned files: {e}") + QMessageBox.warning(self, "Error", f"Failed to scan: {e}") + def closeEvent(self, event): """Handle window close event""" # Stop worker thread if running