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:
|
if not self.is_running:
|
||||||
break
|
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}...")
|
self.progress_signal.emit(progress, f"Converting {track.title}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -207,14 +207,18 @@ class WorkerThread(QThread):
|
|||||||
if not self.is_running:
|
if not self.is_running:
|
||||||
break
|
break
|
||||||
|
|
||||||
progress = int((i / len(tracks)) * 100)
|
progress = int(((i + 1) / len(tracks)) * 100)
|
||||||
self.progress_signal.emit(progress, f"Transferring {track.title}...")
|
self.progress_signal.emit(progress, f"Transferring {track.title}...")
|
||||||
|
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
# Transfer file
|
try:
|
||||||
success = ipod.transfer_file(path)
|
# Transfer file
|
||||||
if success:
|
success = ipod.transfer_file(path)
|
||||||
transferred_tracks.append((track, path))
|
if success:
|
||||||
|
transferred_tracks.append((track, path))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error transferring track {track.title}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
# Report success
|
# Report success
|
||||||
self.finished_signal.emit(
|
self.finished_signal.emit(
|
||||||
@ -424,16 +428,34 @@ class MainWindow(QMainWindow):
|
|||||||
device_layout.addWidget(refresh_button)
|
device_layout.addWidget(refresh_button)
|
||||||
layout.addLayout(device_layout)
|
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
|
# Device info
|
||||||
self.device_info_label = QLabel("No device selected")
|
self.device_info_label = QLabel("No device selected")
|
||||||
layout.addWidget(self.device_info_label)
|
layout.addWidget(self.device_info_label)
|
||||||
|
|
||||||
|
# Device status
|
||||||
|
self.device_status_label = QLabel("Status: Not connected")
|
||||||
|
layout.addWidget(self.device_status_label)
|
||||||
|
|
||||||
# Track count
|
# Track count
|
||||||
self.track_count_label = QLabel("Tracks on device: 0")
|
self.track_count_label = QLabel("Tracks on device: 0")
|
||||||
layout.addWidget(self.track_count_label)
|
layout.addWidget(self.track_count_label)
|
||||||
|
|
||||||
# Transfer button
|
# Transfer button
|
||||||
self.transfer_button = QPushButton("Transfer to iPod")
|
self.transfer_button = QPushButton("Transfer to iPod")
|
||||||
|
self.transfer_button.setEnabled(False)
|
||||||
self.transfer_button.clicked.connect(self._on_transfer_clicked)
|
self.transfer_button.clicked.connect(self._on_transfer_clicked)
|
||||||
layout.addWidget(self.transfer_button)
|
layout.addWidget(self.transfer_button)
|
||||||
|
|
||||||
@ -459,6 +481,11 @@ class MainWindow(QMainWindow):
|
|||||||
remove_button = QPushButton("Remove Selected from iPod")
|
remove_button = QPushButton("Remove Selected from iPod")
|
||||||
remove_button.clicked.connect(self._on_remove_tracks_clicked)
|
remove_button.clicked.connect(self._on_remove_tracks_clicked)
|
||||||
layout.addWidget(remove_button)
|
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):
|
def _setup_settings_tab(self, tab):
|
||||||
"""Set up the settings tab"""
|
"""Set up the settings tab"""
|
||||||
@ -712,54 +739,161 @@ class MainWindow(QMainWindow):
|
|||||||
# Update device combo box
|
# Update device combo box
|
||||||
self.device_combo.clear()
|
self.device_combo.clear()
|
||||||
for device in self.ipod_devices:
|
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
|
# Try to mount the first device
|
||||||
if self.ipod_devices:
|
if self.ipod_devices:
|
||||||
device_id = self.ipod_devices[0]["id"]
|
device = self.ipod_devices[0]
|
||||||
detected_mount = self.ipod_devices[0].get("mount_point")
|
device_id = device["id"]
|
||||||
ipod = IPodDevice()
|
detected_mount = device.get("mount_point")
|
||||||
mount_point = ipod.mount_device(device_id, detected_mount=detected_mount)
|
already_mounted = device.get("mounted", False)
|
||||||
|
|
||||||
if mount_point:
|
if already_mounted and detected_mount:
|
||||||
# Get device info
|
# Device already mounted
|
||||||
self.current_mount_point = mount_point
|
self._set_device_mounted(device, detected_mount)
|
||||||
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()
|
|
||||||
else:
|
else:
|
||||||
self.device_info_label.setText(
|
# Device not mounted - auto-mount
|
||||||
f"Device: {self.ipod_devices[0]['name']}\n"
|
ipod = IPodDevice()
|
||||||
f"Failed to mount device. Please check permissions."
|
mount_point = ipod.mount_device(device_id, detected_mount=detected_mount)
|
||||||
)
|
|
||||||
self.transfer_button.setEnabled(False)
|
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)
|
self.statusBar().showMessage(message)
|
||||||
else:
|
else:
|
||||||
self.device_info_label.setText("No iPod devices found")
|
self.device_info_label.setText("No iPod devices found")
|
||||||
|
self.device_status_label.setText("Status: Not connected")
|
||||||
self.device_combo.clear()
|
self.device_combo.clear()
|
||||||
|
self.mount_button.setEnabled(False)
|
||||||
|
self.eject_button.setEnabled(False)
|
||||||
self.transfer_button.setEnabled(False)
|
self.transfer_button.setEnabled(False)
|
||||||
|
self.track_count_label.setText("Tracks on device: 0")
|
||||||
self.statusBar().showMessage(message)
|
self.statusBar().showMessage(message)
|
||||||
|
|
||||||
# Clean up worker thread
|
# Clean up worker thread
|
||||||
self.worker_thread = None
|
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):
|
def _on_transfer_clicked(self):
|
||||||
"""Handle transfer button click"""
|
"""Handle transfer button click"""
|
||||||
if not self.converted_tracks:
|
if not self.converted_tracks:
|
||||||
@ -872,6 +1006,50 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to remove tracks: {e}")
|
logger.error(f"Failed to remove tracks: {e}")
|
||||||
QMessageBox.warning(self, "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):
|
def closeEvent(self, event):
|
||||||
"""Handle window close event"""
|
"""Handle window close event"""
|
||||||
# Stop worker thread if running
|
# Stop worker thread if running
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user