Add Transfer tab: Mount/Eject UI, track management, orphaned file scanning

- Add Mount/Eject buttons with device status display
- Auto-mount device on refresh
- Load and display tracks from iPod SQLite database
- Delete selected tracks from iPod with confirmation
- Scan for orphaned files (audio files without DB entries)
- Bulk delete orphaned files to reclaim space
- Show track count and free/total space
This commit is contained in:
Maksim Totmin 2026-05-31 14:39:18 +07:00
parent 762e9dffe8
commit 33c8534eb4

View File

@ -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