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:
parent
762e9dffe8
commit
33c8534eb4
254
src/main.py
254
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user