Vendored 5 packages from iOpenPod (TheRealSavi/iOpenPod): iTunesDB_Writer/ — binary iTunesDB generation iTunesDB_Shared/ — field defs, constants, album identity SQLiteDB_Writer/ — SQLite databases for Nano 6G/7G iTunesDB_Parser/ — play stats reading from iPod ipod_device/ — device detection, FireWire ID, capabilities Rationale: iOpenPod is a standalone iPod manager app, not a library. Pulling it as a pip dependency is heavy and confusing for users. Changes: + src/vendor/ — 77 .py files + calcHashAB.wasm (506 KB) ~ ipod_nano7_db.py — _find_iop_root() now returns src/vendor/ ~ requirements.txt — iopenpod replaced with wasmtime + pycryptodome ~ .gitignore — removed iOpenPod/ exclusion (setup.py reads requirements.txt automatically)
212 lines
6.8 KiB
Python
212 lines
6.8 KiB
Python
"""
|
|
Read iPod SysInfoExtended via Apple's USB vendor-control command.
|
|
|
|
libgpod uses this path for devices such as later nanos that can expose richer
|
|
SysInfoExtended data over a device-level vendor request than through SCSI VPD.
|
|
It is intentionally separate from ``vpd_libusb``'s Mass Storage Bulk-Only/SCSI
|
|
path so callers can preserve transport provenance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from .diagnostic_log import CAPABILITY_FIELDS, IDENTITY_FIELDS, format_fields
|
|
from .models import IPOD_USB_PIDS
|
|
from .sysinfo import parse_sysinfo_extended
|
|
from .usb_backend import backend_diagnostic, get_libusb_backend
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
APPLE_VID = 0x05AC
|
|
|
|
_REQUEST_TYPE_IN_VENDOR_DEVICE = 0xC0
|
|
_REQUEST_READ_SYSINFO_EXTENDED = 0x40
|
|
_VALUE_SYSINFO_EXTENDED = 0x0002
|
|
_CHUNK_SIZE = 0x1000
|
|
_MAX_CHUNKS = 0xFFFF
|
|
_TIMEOUT_MS = 5000
|
|
|
|
|
|
def _find_ipod_devices() -> list:
|
|
try:
|
|
import usb.core
|
|
except ImportError:
|
|
logger.info("USB vendor SysInfoExtended: pyusb not installed")
|
|
return []
|
|
|
|
backend = get_libusb_backend()
|
|
if backend is None:
|
|
logger.info(
|
|
"USB vendor SysInfoExtended: no PyUSB backend available: %s",
|
|
backend_diagnostic(),
|
|
)
|
|
return []
|
|
|
|
found = usb.core.find(find_all=True, idVendor=APPLE_VID, backend=backend)
|
|
if found is None:
|
|
return []
|
|
return [dev for dev in found if getattr(dev, "idProduct", None) in IPOD_USB_PIDS]
|
|
|
|
|
|
def _device_serial(dev) -> str:
|
|
try:
|
|
return (dev.serial_number or "").replace(" ", "").strip().upper()
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _read_sysinfo_extended_from_device(dev) -> bytes:
|
|
raw = bytearray()
|
|
|
|
for index in range(_MAX_CHUNKS):
|
|
chunk = dev.ctrl_transfer(
|
|
_REQUEST_TYPE_IN_VENDOR_DEVICE,
|
|
_REQUEST_READ_SYSINFO_EXTENDED,
|
|
_VALUE_SYSINFO_EXTENDED,
|
|
index,
|
|
_CHUNK_SIZE,
|
|
timeout=_TIMEOUT_MS,
|
|
)
|
|
data = bytes(chunk)
|
|
raw.extend(data)
|
|
if len(data) != _CHUNK_SIZE:
|
|
break
|
|
|
|
return bytes(raw).rstrip(b"\x00")
|
|
|
|
|
|
def query_ipod_usb_sysinfo_extended(
|
|
usb_pid: int = 0,
|
|
serial_filter: str = "",
|
|
) -> dict | None:
|
|
"""Query one iPod through Apple's USB vendor-control SysInfo command.
|
|
|
|
Returns a dict shaped similarly to the SCSI VPD readers, with parsed
|
|
SysInfoExtended keys plus ``vpd_raw_xml``, ``usb_pid``, ``usb_serial`` and
|
|
provenance metadata. Returns ``None`` if no matching iPod answers.
|
|
"""
|
|
# _find_ipod_devices already checks for pyusb availability and the backend.
|
|
# No need to import usb.core again here.
|
|
|
|
serial_filter = serial_filter.replace(" ", "").strip().upper()
|
|
logger.info(
|
|
"USB vendor SysInfoExtended query start: pid=%s filter=%s",
|
|
f"0x{usb_pid:04X}" if usb_pid else "any",
|
|
serial_filter or "none",
|
|
)
|
|
candidates = [
|
|
dev for dev in _find_ipod_devices()
|
|
if not usb_pid or getattr(dev, "idProduct", None) == usb_pid
|
|
]
|
|
logger.info(
|
|
"USB vendor SysInfoExtended candidates: count=%d pids=%s",
|
|
len(candidates),
|
|
", ".join(f"0x{getattr(dev, 'idProduct', 0):04X}" for dev in candidates) or "none",
|
|
)
|
|
target = None
|
|
if serial_filter:
|
|
for dev in candidates:
|
|
serial = _device_serial(dev)
|
|
if serial == serial_filter:
|
|
target = dev
|
|
break
|
|
if target is None and len(candidates) == 1 and usb_pid:
|
|
# Some Windows libusb driver stacks enumerate the device but refuse
|
|
# string descriptors. If PID has already narrowed this to a single
|
|
# candidate, it is still safe to attempt the read.
|
|
target = candidates[0]
|
|
elif candidates:
|
|
target = candidates[0]
|
|
|
|
if target is None:
|
|
logger.info("USB vendor SysInfoExtended query: no matching target")
|
|
return None
|
|
|
|
usb_serial = _device_serial(target)
|
|
try:
|
|
raw_xml = _read_sysinfo_extended_from_device(target)
|
|
except Exception as exc:
|
|
message = str(exc)
|
|
if "not supported" in message.lower() or "not implemented" in message.lower():
|
|
logger.info(
|
|
"USB vendor SysInfoExtended backend is available, but Windows "
|
|
"driver access does not support device control transfers for "
|
|
"PID=0x%04X serial=%s: %s",
|
|
getattr(target, "idProduct", 0),
|
|
usb_serial,
|
|
exc,
|
|
)
|
|
else:
|
|
logger.info(
|
|
"USB vendor SysInfoExtended read failed for PID=0x%04X serial=%s: %s",
|
|
getattr(target, "idProduct", 0),
|
|
usb_serial,
|
|
exc,
|
|
)
|
|
return None
|
|
|
|
if not raw_xml:
|
|
logger.info(
|
|
"USB vendor SysInfoExtended query returned empty payload: "
|
|
"PID=0x%04X serial=%s",
|
|
getattr(target, "idProduct", 0),
|
|
usb_serial,
|
|
)
|
|
return None
|
|
|
|
parsed = parse_sysinfo_extended(raw_xml, source="usb_vendor", live=True)
|
|
if not parsed.plist:
|
|
logger.info(
|
|
"USB vendor SysInfoExtended parse returned no plist keys: "
|
|
"PID=0x%04X serial=%s",
|
|
getattr(target, "idProduct", 0),
|
|
usb_serial,
|
|
)
|
|
return None
|
|
|
|
result: dict = {
|
|
"usb_pid": getattr(target, "idProduct", 0),
|
|
"usb_serial": usb_serial,
|
|
"vpd_raw_xml": parsed.raw_xml or raw_xml,
|
|
"_source": "usb_vendor",
|
|
"_transport": "usb_vendor_control",
|
|
"_used_usb_vendor": True,
|
|
}
|
|
result.update(parsed.plist)
|
|
log_identity = dict(parsed.identity)
|
|
log_identity["usb_pid"] = getattr(target, "idProduct", 0)
|
|
if usb_serial:
|
|
log_identity["usb_serial"] = usb_serial
|
|
|
|
logger.info(
|
|
"USB vendor SysInfoExtended query successful: PID=0x%04X serial=%s "
|
|
"keys=%d identity=[%s] caps=[%s]",
|
|
getattr(target, "idProduct", 0),
|
|
usb_serial,
|
|
len(parsed.plist),
|
|
format_fields(log_identity, IDENTITY_FIELDS),
|
|
format_fields(log_identity, CAPABILITY_FIELDS, include_false=True),
|
|
)
|
|
return result
|
|
|
|
|
|
def query_all_ipod_usb_sysinfo_extended() -> list[dict]:
|
|
"""Query every connected iPod that answers the vendor-control command."""
|
|
results: list[dict] = []
|
|
for dev in _find_ipod_devices():
|
|
try:
|
|
info = query_ipod_usb_sysinfo_extended(
|
|
usb_pid=getattr(dev, "idProduct", 0),
|
|
serial_filter=_device_serial(dev),
|
|
)
|
|
if info:
|
|
results.append(info)
|
|
except Exception as exc:
|
|
logger.debug(
|
|
"USB vendor SysInfoExtended query failed for PID=0x%04X: %s",
|
|
getattr(dev, "idProduct", 0),
|
|
exc,
|
|
)
|
|
return results
|