Rewrite device handling: udisksctl mount/unmount, Nano 7 database integration

- Replace libimobiledevice dependency with udisksctl (works without root)
- Detect block devices (/dev/sdX) for iPod partitions
- Add mount_device() using udisksctl mount -b
- Add unmount_device() using udisksctl unmount + power-off
- Update transfer_file() to use Nano7Database.add_track()
- Handle duplicate skips (add_track returns None)
- Fall back to simple file copy if Nano 7 DB unavailable
This commit is contained in:
Maksim Totmin 2026-05-31 14:39:11 +07:00
parent 11c40c8a99
commit 762e9dffe8

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
iPod Device Handler Module for iPod Nano Transfer Tool iPod Device Handler Module for iPod Nano Transfer Tool
Handles device detection and file transfer to iPod Nano Handles device detection, mounting, and file transfer to iPod Nano
""" """
import os import os
import sys import sys
import json
import logging import logging
import platform import platform
import shutil import shutil
@ -31,37 +32,9 @@ class IPodDevice:
self.mount_point = mount_point self.mount_point = mount_point
self.system = platform.system() self.system = platform.system()
self.device_info = {} self.device_info = {}
self.logger = logger # Add this line self._block_device = None # /dev/sdd1 etc.
self._parent_drive = None # /dev/sdd (for power-off)
# Check if libimobiledevice is installed self.logger = logger
if not self._check_dependencies():
self.logger.warning("Some dependencies are missing. Device detection may not work properly.")
def _check_dependencies(self) -> bool:
"""
Check if required dependencies are installed
Returns:
True if all dependencies are installed, False otherwise
"""
try:
if self.system == "Linux":
# Check for ifuse and libimobiledevice
subprocess.run(["which", "ifuse"], check=True, capture_output=True)
subprocess.run(["which", "idevice_id"], check=True, capture_output=True)
return True
elif self.system == "Darwin": # macOS
# Check for libimobiledevice
subprocess.run(["which", "idevice_id"], check=True, capture_output=True)
return True
elif self.system == "Windows":
# Check for libimobiledevice-win32 (simplified check)
# In reality, this would need a more complex check
return os.path.exists("C:\\Program Files\\libimobiledevice")
return False
except subprocess.SubprocessError:
return False
def detect_devices(self) -> List[Dict[str, str]]: def detect_devices(self) -> List[Dict[str, str]]:
""" """
@ -71,234 +44,223 @@ class IPodDevice:
List of dictionaries with device information List of dictionaries with device information
""" """
devices = [] devices = []
# Check platform
system = platform.system() system = platform.system()
try: try:
if system == "Darwin": # macOS if system == "Linux":
# Try using libimobiledevice if available devices = self._detect_linux()
try: elif system == "Darwin":
result = subprocess.run( devices = self._detect_macos()
["idevice_id", "-l"],
capture_output=True,
check=False,
text=True
)
if result.returncode == 0 and result.stdout.strip():
# Parse device IDs
device_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
for device_id in device_ids:
# Get device info
info_result = subprocess.run(
["ideviceinfo", "-u", device_id],
capture_output=True,
check=False,
text=True
)
if info_result.returncode == 0:
# Parse device info
device_info = {}
for line in info_result.stdout.splitlines():
if ":" in line:
key, value = line.split(":", 1)
device_info[key.strip()] = value.strip()
if "ProductType" in device_info and "iPod" in device_info["ProductType"]:
devices.append({
"id": device_id,
"name": device_info.get("DeviceName", "iPod"),
"model": device_info.get("ProductType", "Unknown"),
"mount_point": self._get_ipod_mount_point(device_id)
})
except FileNotFoundError:
# libimobiledevice not installed, fall back to disk utility
self.logger.warning("libimobiledevice not found, falling back to disk utility")
# Fall back to diskutil list
result = subprocess.run(
["diskutil", "list"],
capture_output=True,
check=False,
text=True
)
if result.returncode == 0:
# Look for iPod in the output
for line in result.stdout.splitlines():
if "iPod" in line:
# Extract disk identifier
parts = line.split()
for i, part in enumerate(parts):
if part.startswith("/dev/"):
disk_id = part
# Get mount point
mount_result = subprocess.run(
["diskutil", "info", disk_id],
capture_output=True,
check=False,
text=True
)
if mount_result.returncode == 0:
mount_point = None
for info_line in mount_result.stdout.splitlines():
if "Mount Point" in info_line:
mount_point = info_line.split(":", 1)[1].strip()
break
if mount_point and os.path.exists(mount_point):
devices.append({
"id": disk_id,
"name": "iPod",
"model": "Unknown",
"mount_point": mount_point
})
# If no devices found, check for mounted volumes with iPod in the name
if not devices:
volumes_dir = "/Volumes"
if os.path.exists(volumes_dir):
for volume in os.listdir(volumes_dir):
volume_path = os.path.join(volumes_dir, volume)
if "iPod" in volume and os.path.isdir(volume_path):
devices.append({
"id": "unknown",
"name": volume,
"model": "Unknown",
"mount_point": volume_path
})
elif system == "Windows": elif system == "Windows":
# On Windows, check for mounted drives with iPod in the label devices = self._detect_windows()
import win32api
import win32con
try:
drives = win32api.GetLogicalDriveStrings().split('\0')[:-1]
for drive in drives:
try:
volume_name = win32api.GetVolumeInformation(drive)[0]
if volume_name and "iPod" in volume_name:
devices.append({
"id": drive[0],
"name": volume_name,
"model": "Unknown",
"mount_point": drive
})
except:
pass
except ImportError:
self.logger.warning("win32api module not available, cannot detect iPod on Windows")
# Fallback to checking drive letters
import string
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
# Check if this looks like an iPod
if os.path.exists(os.path.join(drive, "iPod_Control")):
devices.append({
"id": letter,
"name": "iPod",
"model": "Unknown",
"mount_point": drive
})
elif system == "Linux":
# On Linux, check mounted devices
try:
result = subprocess.run(
["lsblk", "-o", "NAME,LABEL,MOUNTPOINT", "-J"],
capture_output=True,
check=False,
text=True
)
if result.returncode == 0:
import json
try:
data = json.loads(result.stdout)
for device in data.get("blockdevices", []):
if "children" in device:
for child in device["children"]:
label = child.get("label", "")
mountpoint = child.get("mountpoint")
if mountpoint and label and "ipod" in label.lower():
devices.append({
"id": child.get("name", "unknown"),
"name": label,
"model": "Unknown",
"mount_point": mountpoint
})
except json.JSONDecodeError:
self.logger.warning("Failed to parse lsblk output as JSON")
except FileNotFoundError:
self.logger.warning("lsblk command not found")
# Fallback to checking /media and /mnt directories
if not devices:
for mount_dir in ["/media", "/mnt", "/run/media"]:
if os.path.exists(mount_dir):
for user_dir in os.listdir(mount_dir):
user_path = os.path.join(mount_dir, user_dir)
if os.path.isdir(user_path):
for device_dir in os.listdir(user_path):
if "ipod" in device_dir.lower():
device_path = os.path.join(user_path, device_dir)
if os.path.isdir(device_path):
devices.append({
"id": "unknown",
"name": device_dir,
"model": "Unknown",
"mount_point": device_path
})
except Exception as e: except Exception as e:
self.logger.error(f"Error detecting devices: {e}") self.logger.error(f"Error detecting devices: {e}")
# If no devices found through system methods, check for manual mount point # If no devices found, check for manual mount point
if not devices and self.mount_point and os.path.exists(self.mount_point): if not devices and self.mount_point and os.path.exists(self.mount_point):
devices.append({ devices.append({
"id": "manual", "id": "manual",
"name": os.path.basename(self.mount_point), "name": os.path.basename(self.mount_point),
"model": "Unknown", "model": "Unknown",
"mount_point": self.mount_point "mount_point": self.mount_point,
"mounted": True,
}) })
return devices return devices
def _get_ipod_mount_point(self, device_id: str) -> Optional[str]: def _detect_linux(self) -> List[Dict[str, str]]:
""" """Detect iPod on Linux using lsblk and udisksctl"""
Get the mount point for an iPod device devices = []
Args: # Method 1: lsblk JSON output - find devices with IPOD label
device_id: Device ID try:
result = subprocess.run(
["lsblk", "-o", "NAME,LABEL,MOUNTPOINT,TYPE", "-J"],
capture_output=True, check=False, text=True
)
Returns: if result.returncode == 0:
Mount point if found, None otherwise data = json.loads(result.stdout)
""" for device in data.get("blockdevices", []):
# This method is not implemented in the provided code if device.get("type") != "disk":
# You may need to implement it according to your requirements continue
parent_name = device.get("name", "")
for child in device.get("children", []):
label = child.get("label", "")
mountpoint = child.get("mountpoint")
child_name = child.get("name", "")
if label and "ipod" in label.lower():
dev_path = f"/dev/{child_name}"
parent_dev = f"/dev/{parent_name}"
device_entry = {
"id": dev_path,
"name": label,
"model": "iPod Nano 7",
"mount_point": mountpoint,
"mounted": mountpoint is not None,
"_parent_drive": parent_dev,
}
devices.append(device_entry)
except (FileNotFoundError, json.JSONDecodeError) as e:
self.logger.warning(f"lsblk JSON failed: {e}")
# Method 2: udisksctl dump - find iPod drives
if not devices:
try:
result = subprocess.run(
["udisksctl", "dump"],
capture_output=True, check=False, text=True
)
if result.returncode == 0:
current_block = None
current_drive = None
current_label = None
current_mount = None
for line in result.stdout.splitlines():
if "/org/freedesktop/UDisks2/block_devices/" in line:
# Save previous if it was an iPod
if current_block and current_label and "ipod" in current_label.lower():
devices.append({
"id": current_block,
"name": current_label,
"model": "iPod",
"mount_point": current_mount if current_mount else None,
"mounted": current_mount is not None,
"_parent_drive": self._get_parent_drive_from_udisks(current_block),
})
current_block = line.split("/block_devices/")[-1].strip().rstrip(":")
current_drive = None
current_label = None
current_mount = None
if "IdLabel:" in line and current_block:
val = line.split(":", 1)[1].strip()
if val and val != "''":
current_label = val.strip("'")
if "MountPoints:" in line and current_block:
val = line.split(":", 1)[1].strip()
if val and val != "''" and val != "b''":
# MountPoints is a byte array like b'/run/media/mat/IPOD'
import ast
try:
decoded = ast.literal_eval(val)
if isinstance(decoded, bytes):
current_mount = decoded.decode("utf-8", errors="replace")
elif isinstance(decoded, str):
current_mount = decoded
except (ValueError, SyntaxError):
current_mount = val.strip("'\"")
# Don't forget the last entry
if current_block and current_label and "ipod" in current_label.lower():
devices.append({
"id": current_block,
"name": current_label,
"model": "iPod",
"mount_point": current_mount if current_mount else None,
"mounted": current_mount is not None,
"_parent_drive": self._get_parent_drive_from_udisks(current_block),
})
except FileNotFoundError:
self.logger.warning("udisksctl not found")
# Method 3: Check common mount directories for already-mounted iPods
for device in devices:
if not device["mounted"]:
for base in ["/run/media", "/media", "/mnt"]:
if os.path.exists(base):
for user_or_device in os.listdir(base):
user_path = os.path.join(base, user_or_device)
if os.path.isdir(user_path):
# Could be /run/media/mat/IPOD or /media/IPOD
check_paths = [user_path]
if os.path.isdir(user_path):
for sub in os.listdir(user_path):
sub_path = os.path.join(user_path, sub)
if "ipod" in sub.lower() and os.path.isdir(sub_path):
check_paths.append(sub_path)
for check_path in check_paths:
if os.path.exists(os.path.join(check_path, "iPod_Control")):
device["mount_point"] = check_path
device["mounted"] = True
break
if device["mounted"]:
break
if device["mounted"]:
break
return devices
def _get_parent_drive_from_udisks(self, block_name: str) -> Optional[str]:
"""Get parent drive device from udisksctl for a partition"""
try:
# /dev/sdd1 -> /dev/sdd
if block_name and block_name.startswith("/dev/"):
name = block_name[5:] # sdd1
# Strip trailing partition number
base = name.rstrip("0123456789")
if base:
return f"/dev/{base}"
except Exception:
pass pass
return None
def _detect_macos(self) -> List[Dict[str, str]]:
"""Detect iPod on macOS"""
devices = []
# Check /Volumes for iPod
volumes_dir = "/Volumes"
if os.path.exists(volumes_dir):
for volume in os.listdir(volumes_dir):
volume_path = os.path.join(volumes_dir, volume)
if "iPod" in volume and os.path.isdir(volume_path):
if os.path.exists(os.path.join(volume_path, "iPod_Control")):
devices.append({
"id": "unknown",
"name": volume,
"model": "Unknown",
"mount_point": volume_path,
"mounted": True,
})
return devices
def _detect_windows(self) -> List[Dict[str, str]]:
"""Detect iPod on Windows"""
devices = []
import string
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
if os.path.exists(os.path.join(drive, "iPod_Control")):
devices.append({
"id": letter,
"name": "iPod",
"model": "Unknown",
"mount_point": drive,
"mounted": True,
})
return devices
def mount_device(self, device_id: Optional[str] = None, detected_mount: Optional[str] = None) -> Optional[str]: def mount_device(self, device_id: Optional[str] = None, detected_mount: Optional[str] = None) -> Optional[str]:
""" """
Mount iPod device Mount iPod device using udisksctl (Linux) or verify existing mount
Args: Args:
device_id: Device ID (optional) device_id: Device path like /dev/sdd1
detected_mount: Already detected mount point (optional) detected_mount: Already detected mount point
Returns: Returns:
Mount point if successful, None otherwise Mount point if successful, None otherwise
""" """
# If device is already mounted at detected path, just use it # If device is already mounted at detected path, just use it
if detected_mount and os.path.exists(detected_mount): if detected_mount and os.path.exists(detected_mount):
# Verify it looks like an iPod
if os.path.exists(os.path.join(detected_mount, "iPod_Control")): if os.path.exists(os.path.join(detected_mount, "iPod_Control")):
self.mount_point = detected_mount self.mount_point = detected_mount
self.logger.info(f"Using already mounted device at: {detected_mount}") self.logger.info(f"Using already mounted device at: {detected_mount}")
@ -310,104 +272,201 @@ class IPodDevice:
try: try:
if self.system == "Linux": if self.system == "Linux":
# Check common mount points for already-mounted USB storage return self._mount_linux(device_id)
for base in ["/run/media", "/media", "/mnt"]:
if os.path.exists(base):
for user_dir in os.listdir(base):
user_path = os.path.join(base, user_dir)
if os.path.isdir(user_path):
if os.path.exists(os.path.join(user_path, "iPod_Control")):
self.mount_point = user_path
self.logger.info(f"Found already mounted iPod at: {user_path}")
return user_path
# Try to mount with ifuse if available
if shutil.which("ifuse"):
# Create mount point
mount_point = "/tmp/ipod_mount"
os.makedirs(mount_point, exist_ok=True)
# Mount with ifuse
cmd = ["ifuse", mount_point]
if device_id:
cmd.extend(["-u", device_id])
subprocess.run(cmd, check=True)
self.mount_point = mount_point
self.logger.info(f"Device mounted at: {mount_point}")
return mount_point
else:
self.logger.error("ifuse not installed and device not already mounted")
return None
elif self.system == "Darwin": elif self.system == "Darwin":
# On macOS, devices are automatically mounted return self._mount_macos()
# Find the iPod mount point
result = subprocess.run(
["df", "-h"],
check=True,
capture_output=True,
text=True
)
for line in result.stdout.strip().split('\n'):
if "iPod" in line:
# Extract disk identifier
parts = line.split()
if len(parts) >= 9: # macOS df output format
mount_point = parts[8]
self.mount_point = mount_point
self.logger.info(f"Device found at: {mount_point}")
return mount_point
self.logger.warning("iPod mount point not found")
return None
elif self.system == "Windows": elif self.system == "Windows":
# Windows implementation would go here return self._mount_windows()
# This is a placeholder for future implementation
self.logger.warning("Windows mounting not fully implemented")
return None
except subprocess.SubprocessError as e: except subprocess.SubprocessError as e:
self.logger.error(f"Failed to mount device: {e}") self.logger.error(f"Failed to mount device: {e}")
return None return None
return None
def _mount_linux(self, device_id: Optional[str]) -> Optional[str]:
"""Mount on Linux using udisksctl"""
# device_id should be like /dev/sdd1
if not device_id or not device_id.startswith("/dev/"):
self.logger.error("No valid device path provided for mounting")
return None
if not os.path.exists(device_id):
self.logger.error(f"Device {device_id} does not exist")
return None
try:
result = subprocess.run(
["udisksctl", "mount", "-b", device_id],
capture_output=True, check=False, text=True
)
if result.returncode != 0:
self.logger.error(f"udisksctl mount failed: {result.stderr}")
return None
# Parse mount point from output: "Mounted /dev/sdd1 at /run/media/mat/IPOD.\n"
output = result.stdout.strip()
mount_point = None
for line in output.splitlines():
if "Mounted" in line and "at" in line:
# "Mounted /dev/sdd1 at /run/media/mat/IPOD."
parts = line.split(" at ")
if len(parts) >= 2:
mp = parts[1].strip().rstrip(".")
if os.path.exists(mp):
mount_point = mp
if not mount_point:
# Fallback: check /run/media for the device
for user_dir in os.listdir("/run/media"):
user_path = os.path.join("/run/media", user_dir)
if os.path.isdir(user_path):
for d in os.listdir(user_path):
d_path = os.path.join(user_path, d)
if os.path.exists(os.path.join(d_path, "iPod_Control")):
mount_point = d_path
break
if mount_point:
break
if mount_point:
self.mount_point = mount_point
self._block_device = device_id
# Extract parent drive: /dev/sdd1 -> /dev/sdd
name = device_id[5:]
self._parent_drive = f"/dev/{name.rstrip('0123456789')}"
self.logger.info(f"Device mounted at: {mount_point}")
return mount_point
self.logger.error("Could not determine mount point")
return None
except FileNotFoundError:
self.logger.error("udisksctl not found. Please install udisks2.")
return None
def _mount_macos(self) -> Optional[str]:
"""Mount on macOS - devices auto-mount, just find the mount point"""
result = subprocess.run(
["diskutil", "list"],
capture_output=True, check=False, text=True
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if "iPod" in line:
df_result = subprocess.run(
["df", "-h"],
capture_output=True, check=False, text=True
)
for df_line in df_result.stdout.splitlines():
if "iPod" in df_line:
parts = df_line.split()
if len(parts) >= 9:
mount_point = parts[8]
self.mount_point = mount_point
return mount_point
return None
def _mount_windows(self) -> Optional[str]:
"""Mount on Windows"""
self.logger.warning("Windows mounting not fully implemented")
return None
def unmount_device(self) -> bool: def unmount_device(self) -> bool:
""" """
Unmount iPod device Unmount and safely eject iPod device
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
if not self.mount_point: if not self.mount_point and not self._block_device:
self.logger.warning("No device mounted") self.logger.warning("No device mounted")
return False return False
try: try:
if self.system == "Linux": if self.system == "Linux":
# Unmount with fusermount return self._unmount_linux()
subprocess.run(["fusermount", "-u", self.mount_point], check=True)
self.logger.info(f"Device unmounted: {self.mount_point}")
self.mount_point = None
return True
elif self.system == "Darwin": elif self.system == "Darwin":
# On macOS, use diskutil return self._unmount_macos()
elif self.system == "Windows":
return self._unmount_windows()
except subprocess.SubprocessError as e:
self.logger.error(f"Failed to unmount device: {e}")
return False
return False
def _unmount_linux(self) -> bool:
"""Unmount and power-off on Linux using udisksctl"""
success = True
# Step 1: Unmount
if self.mount_point or self._block_device:
device = self._block_device
if not device:
# Try to find the device from mount point
result = subprocess.run(
["findmnt", "-n", "-o", "SOURCE", self.mount_point],
capture_output=True, check=False, text=True
)
if result.returncode == 0 and result.stdout.strip():
device = result.stdout.strip()
if device:
result = subprocess.run(
["udisksctl", "unmount", "-b", device],
capture_output=True, check=False, text=True
)
if result.returncode == 0:
self.logger.info(f"Device unmounted: {device}")
else:
self.logger.warning(f"Unmount warning: {result.stderr}")
# Not a fatal error - device might already be unmounted
# Step 2: Power off the drive (safely remove)
drive = self._parent_drive
if not drive and self._block_device:
name = self._block_device[5:]
drive = f"/dev/{name.rstrip('0123456789')}"
if drive:
result = subprocess.run(
["udisksctl", "power-off", "-b", drive],
capture_output=True, check=False, text=True
)
if result.returncode == 0:
self.logger.info(f"Device powered off: {drive}")
else:
self.logger.warning(f"Power-off warning: {result.stderr}")
# Not fatal - USB may have been unplugged already
self.mount_point = None
self._block_device = None
self._parent_drive = None
return success
def _unmount_macos(self) -> bool:
"""Eject on macOS"""
if self.mount_point:
subprocess.run(["diskutil", "eject", self.mount_point], check=True) subprocess.run(["diskutil", "eject", self.mount_point], check=True)
self.logger.info(f"Device ejected: {self.mount_point}") self.logger.info(f"Device ejected: {self.mount_point}")
self.mount_point = None self.mount_point = None
return True return True
return False
elif self.system == "Windows": def _unmount_windows(self) -> bool:
# Windows implementation would go here """Unmount on Windows"""
# This is a placeholder for future implementation
self.logger.warning("Windows unmounting not fully implemented") self.logger.warning("Windows unmounting not fully implemented")
return False return False
except subprocess.SubprocessError as e: def is_mounted(self) -> bool:
self.logger.error(f"Failed to unmount device: {e}") """Check if iPod is currently mounted"""
if self.mount_point and os.path.exists(self.mount_point):
return os.path.exists(os.path.join(self.mount_point, "iPod_Control"))
return False return False
def get_device_info(self) -> Dict[str, Any]: def get_device_info(self) -> Dict[str, Any]:
@ -443,18 +502,6 @@ class IPodDevice:
info["has_nano7_db"] = True info["has_nano7_db"] = True
except Exception: except Exception:
info["has_nano7_db"] = False info["has_nano7_db"] = False
# Fallback: count files in Music directory
music_dir = os.path.join(self.mount_point, "Music")
if os.path.exists(music_dir):
info["has_music_dir"] = True
music_files = []
for root, _, files in os.walk(music_dir):
for file in files:
if file.lower().endswith(('.m4a', '.mp3', '.aac')):
music_files.append(os.path.join(root, file))
info["music_file_count"] = len(music_files)
else:
info["has_music_dir"] = False
return info return info
@ -487,6 +534,10 @@ class IPodDevice:
from ipod_nano7_db import Nano7Database from ipod_nano7_db import Nano7Database
nano7_db = Nano7Database(self.mount_point) nano7_db = Nano7Database(self.mount_point)
result = nano7_db.add_track(source_file) result = nano7_db.add_track(source_file)
if result is None:
# Track already exists (duplicate)
self.logger.info(f"Track already on iPod, skipping: {os.path.basename(source_file)}")
return True # Not an error - track is already there
self.logger.info(f"Track added to iPod database: {result['title']}") self.logger.info(f"Track added to iPod database: {result['title']}")
return True return True
except ImportError: except ImportError:
@ -541,24 +592,18 @@ class IPodDevice:
if dest_dir: if dest_dir:
full_dest_dir = os.path.join(self.mount_point, dest_dir) full_dest_dir = os.path.join(self.mount_point, dest_dir)
else: else:
# Default to Music directory
full_dest_dir = os.path.join(self.mount_point, "Music") full_dest_dir = os.path.join(self.mount_point, "Music")
# Create destination directory if it doesn't exist
os.makedirs(full_dest_dir, exist_ok=True) os.makedirs(full_dest_dir, exist_ok=True)
# Copy directory contents
for root, dirs, files in os.walk(source_dir): for root, dirs, files in os.walk(source_dir):
# Create relative path
rel_path = os.path.relpath(root, source_dir) rel_path = os.path.relpath(root, source_dir)
if rel_path == '.': if rel_path == '.':
rel_path = '' rel_path = ''
# Create destination subdirectory
dest_subdir = os.path.join(full_dest_dir, rel_path) dest_subdir = os.path.join(full_dest_dir, rel_path)
os.makedirs(dest_subdir, exist_ok=True) os.makedirs(dest_subdir, exist_ok=True)
# Copy files
for file in files: for file in files:
if file.lower().endswith(('.m4a', '.mp3', '.aac', '.mp4')): if file.lower().endswith(('.m4a', '.mp3', '.aac', '.mp4')):
src_file = os.path.join(root, file) src_file = os.path.join(root, file)
@ -575,24 +620,30 @@ class IPodDevice:
if __name__ == "__main__": if __name__ == "__main__":
# Example usage
ipod = IPodDevice() ipod = IPodDevice()
# Detect devices # Detect devices
devices = ipod.detect_devices() devices = ipod.detect_devices()
for device in devices: for device in devices:
print(f"Found device: {device['name']} ({device['model']})") status = "mounted" if device.get("mounted") else "NOT mounted"
print(f"Found device: {device['name']} ({device['model']}) - {status}")
if device.get("mount_point"):
print(f" Mount point: {device['mount_point']}")
print(f" Device: {device['id']}")
# Mount device # Mount and eject if device found
if devices: if devices:
mount_point = ipod.mount_device(devices[0]['id']) device = devices[0]
if not device.get("mounted"):
mount_point = ipod.mount_device(device["id"])
if mount_point: if mount_point:
print(f"Device mounted at: {mount_point}") print(f"Mounted at: {mount_point}")
# Get device info
info = ipod.get_device_info() info = ipod.get_device_info()
print(f"Total space: {info.get('total_space', 0) / (1024**3):.2f} GB") print(f"Free space: {info.get('free_space', 0) / 1024**2:.1f} MB")
print(f"Free space: {info.get('free_space', 0) / (1024**3):.2f} GB")
# Unmount device # Unmount
ipod.unmount_device() ipod.unmount_device()
print("Ejected.")
else:
print(f"Already mounted at: {device['mount_point']}")