diff --git a/src/app.py b/src/app.py index bb904c6..702c525 100644 --- a/src/app.py +++ b/src/app.py @@ -16,6 +16,7 @@ from PyQt6.QtWidgets import ( from PyQt6.QtCore import Qt from config_loader import ConfigLoader +from device_monitor import DeviceMonitor from hotkeys import HotkeyManager from ui.library_tab import LibraryTab from ui.ipod_tab import iPodTab @@ -36,6 +37,7 @@ class MainWindow(QMainWindow): self.config_loader = ConfigLoader() self.hotkey_manager: Optional[HotkeyManager] = None + self.device_monitor: Optional[DeviceMonitor] = None self.ipod_tab_index = 1 self._setup_ui() @@ -46,6 +48,7 @@ class MainWindow(QMainWindow): self.library_tab._scan_library() self.library_tab._resume_playback_if_saved() self.ipod_tab._on_refresh_devices_clicked() + self._start_device_monitor() def _setup_ui(self): main_widget = QWidget() @@ -74,6 +77,7 @@ class MainWindow(QMainWindow): self.settings_tab.hide_library_buttons_changed.connect( self.library_tab.set_buttons_visible ) + self.settings_tab.auto_detect_changed.connect(self._on_auto_detect_toggled) def _on_device_mounted(self, mount_point: str): self.library_tab.on_device_mounted(mount_point) @@ -85,6 +89,23 @@ class MainWindow(QMainWindow): self.tab_widget.setTabVisible(self.ipod_tab_index, False) self.statusBar().showMessage("iPod disconnected") + def _start_device_monitor(self): + auto_detect = self.config_loader.get_boolean("Device", "auto_detect", fallback=True) + self.device_monitor = DeviceMonitor() + if auto_detect: + self.ipod_tab.start_monitoring(self.device_monitor) + self.device_monitor.start() + + def _on_auto_detect_toggled(self, enabled: bool): + if not self.device_monitor: + return + if enabled: + self.ipod_tab.start_monitoring(self.device_monitor) + self.device_monitor.start() + else: + self.device_monitor.stop() + self.ipod_tab.stop_monitoring(self.device_monitor) + def _load_settings(self): config = self.config_loader self.library_tab.load_player_settings(config) @@ -128,6 +149,8 @@ class MainWindow(QMainWindow): self.settings_tab.refresh_shortcuts_table() def closeEvent(self, event): + if self.device_monitor: + self.device_monitor.stop() self._save_settings() event.accept() diff --git a/src/device_monitor.py b/src/device_monitor.py new file mode 100644 index 0000000..e9863c4 --- /dev/null +++ b/src/device_monitor.py @@ -0,0 +1,38 @@ +import logging +from typing import Set + +from PyQt6.QtCore import QObject, QTimer, pyqtSignal + +from ipod_device import IPodDevice + +logger = logging.getLogger(__name__) + + +class DeviceMonitor(QObject): + + state_changed = pyqtSignal(list) + + def __init__(self, parent=None): + super().__init__(parent) + self.timer = QTimer(self) + self.timer.timeout.connect(self._poll) + self._device_ids: Set[str] = set() + + def start(self, interval_ms: int = 3000): + self._device_ids.clear() + self._poll() + self.timer.start(interval_ms) + + def stop(self): + self.timer.stop() + self._device_ids.clear() + + def _poll(self): + try: + devices = IPodDevice().detect_devices() + current_ids = {d["id"] for d in devices} + if current_ids != self._device_ids: + self._device_ids = current_ids + self.state_changed.emit(devices) + except Exception: + logger.exception("DeviceMonitor poll failed") diff --git a/src/ipod_device.py b/src/ipod_device.py index 933d799..3016b41 100644 --- a/src/ipod_device.py +++ b/src/ipod_device.py @@ -294,6 +294,24 @@ class IPodDevice: self.logger.error(f"Device {device_id} does not exist") return None + # Check if already mounted via findmnt (avoids udisksctl AlreadyMounted error) + try: + fm_result = subprocess.run( + ["findmnt", "-n", "-o", "TARGET", device_id], + capture_output=True, check=False, text=True + ) + if fm_result.returncode == 0 and fm_result.stdout.strip(): + mount_point = fm_result.stdout.strip() + if os.path.exists(mount_point): + self.mount_point = mount_point + self._block_device = device_id + name = device_id[5:] + self._parent_drive = f"/dev/{name.rstrip('0123456789')}" + self.logger.info(f"Device already mounted at: {mount_point}") + return mount_point + except Exception: + self.logger.warning("findmnt check failed, falling through to udisksctl") + try: result = subprocess.run( ["udisksctl", "mount", "-b", device_id], diff --git a/src/ui/ipod_tab.py b/src/ui/ipod_tab.py index 11fda8a..54cc3f6 100644 --- a/src/ui/ipod_tab.py +++ b/src/ui/ipod_tab.py @@ -11,6 +11,7 @@ from PyQt6.QtWidgets import ( from PyQt6.QtCore import Qt, pyqtSignal from ipod_device import IPodDevice +from device_monitor import DeviceMonitor from worker import WorkerThread logger = logging.getLogger(__name__) @@ -486,6 +487,20 @@ class iPodTab(QWidget): QMessageBox.information(self, "Duplicates" if success else "Error", message) self._cleanup_worker() + def start_monitoring(self, monitor: DeviceMonitor): + monitor.state_changed.connect(self._on_auto_state_changed) + + def stop_monitoring(self, monitor: DeviceMonitor): + try: + monitor.state_changed.disconnect(self._on_auto_state_changed) + except TypeError: + pass + + def _on_auto_state_changed(self, devices): + if self.worker_thread and self.worker_thread.isRunning(): + return + self._on_device_detection_finished(True, f"Found {len(devices)} devices", devices) + def _cleanup_worker(self): if self.worker_thread: self.worker_thread.wait(3000) diff --git a/src/ui/settings_tab.py b/src/ui/settings_tab.py index e109866..c56a434 100644 --- a/src/ui/settings_tab.py +++ b/src/ui/settings_tab.py @@ -20,6 +20,7 @@ class SettingsTab(QWidget): """Settings tab widget.""" hide_library_buttons_changed = pyqtSignal(bool) + auto_detect_changed = pyqtSignal(bool) def __init__(self, parent=None): super().__init__(parent) @@ -80,8 +81,9 @@ class SettingsTab(QWidget): self.clean_temp_check.setChecked(True) advanced_layout.addWidget(self.clean_temp_check) - self.auto_detect_check = QCheckBox("Auto-Detect iPod on Startup") + self.auto_detect_check = QCheckBox("Auto-Detect iPod") self.auto_detect_check.setChecked(True) + self.auto_detect_check.toggled.connect(self.auto_detect_changed.emit) advanced_layout.addWidget(self.auto_detect_check) self.hide_library_buttons_check = QCheckBox("Hide Library Toolbar Buttons")