ipodsync 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {ipodsync-0.1.2 → ipodsync-0.1.3}/PKG-INFO +52 -1
  2. {ipodsync-0.1.2 → ipodsync-0.1.3}/README.md +51 -0
  3. {ipodsync-0.1.2 → ipodsync-0.1.3}/pyproject.toml +1 -1
  4. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/__init__.py +1 -1
  5. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/cli.py +15 -2
  6. ipodsync-0.1.3/src/ipodsync/sysinfo.py +118 -0
  7. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/transport.py +70 -4
  8. ipodsync-0.1.2/src/ipodsync/sysinfo.py +0 -50
  9. {ipodsync-0.1.2 → ipodsync-0.1.3}/.github/workflows/ci.yml +0 -0
  10. {ipodsync-0.1.2 → ipodsync-0.1.3}/.github/workflows/publish.yml +0 -0
  11. {ipodsync-0.1.2 → ipodsync-0.1.3}/.gitignore +0 -0
  12. {ipodsync-0.1.2 → ipodsync-0.1.3}/LICENSE +0 -0
  13. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/__main__.py +0 -0
  14. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_art_templates.py +0 -0
  15. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_gen.py +0 -0
  16. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_gen2.py +0 -0
  17. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_gen_rt.py +0 -0
  18. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_py.py +0 -0
  19. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_tables.bin +0 -0
  20. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_hashab_tables.py +0 -0
  21. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/_lib_templates.py +0 -0
  22. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/artwork_writer.py +0 -0
  23. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/cbk.py +0 -0
  24. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/export.py +0 -0
  25. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/hashab.py +0 -0
  26. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/importer.py +0 -0
  27. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/library.py +0 -0
  28. {ipodsync-0.1.2 → ipodsync-0.1.3}/src/ipodsync/models.py +0 -0
  29. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/data/ArtworkDB +0 -0
  30. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/data/Locations.itdb +0 -0
  31. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/data/Locations.itdb.cbk +0 -0
  32. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/data/hashab-traces.json +0 -0
  33. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/data/hashab-vectors.json +0 -0
  34. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/test_artwork_writer.py +0 -0
  35. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/test_cbk.py +0 -0
  36. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/test_hashab.py +0 -0
  37. {ipodsync-0.1.2 → ipodsync-0.1.3}/tests/test_hashab_port.py +0 -0
  38. {ipodsync-0.1.2 → ipodsync-0.1.3}/tools/build_gen2.py +0 -0
  39. {ipodsync-0.1.2 → ipodsync-0.1.3}/tools/gen_art_templates.py +0 -0
  40. {ipodsync-0.1.2 → ipodsync-0.1.3}/tools/gen_hashab_tables.py +0 -0
  41. {ipodsync-0.1.2 → ipodsync-0.1.3}/tools/gen_lib_templates.py +0 -0
  42. {ipodsync-0.1.2 → ipodsync-0.1.3}/tools/transpile_hashab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipodsync
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Upload music to an iPod nano 6G/7G without iTunes: SQLite library, hashAB signing, cover art (pure-Python).
5
5
  Project-URL: Homepage, https://github.com/buldiei/ipodsync
6
6
  Project-URL: Repository, https://github.com/buldiei/ipodsync
@@ -104,6 +104,57 @@ The iPod is discovered under `/Volumes` (macOS) and `/media`, `/run/media`, `/mn
104
104
  (Linux) by the presence of `iPod_Control/`. To point at it explicitly, set
105
105
  `IPODSYNC_MOUNT=/path/to/mount`.
106
106
 
107
+ ## Linux: mounting the iPod
108
+
109
+ On macOS, Finder mounts the iPod automatically. A headless Linux box usually
110
+ doesn't — the device shows up as a disk but stays unmounted, so `ipodsync` reports
111
+ "iPod not found". Mount it yourself, then point `IPODSYNC_MOUNT` at it.
112
+
113
+ Find the iPod's partition (an `hfsplus` one, roughly the iPod's size):
114
+
115
+ ```bash
116
+ lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT # e.g. sda2 14.7G hfsplus
117
+ ```
118
+
119
+ For read commands (`list` / `export`), the quickest way is `--mount`, which mounts
120
+ the iPod for you via `udisksctl` (no root) and runs the command:
121
+
122
+ ```bash
123
+ ipodsync --mount list
124
+ ```
125
+
126
+ Or mount it yourself:
127
+
128
+ ```bash
129
+ sudo modprobe hfsplus
130
+ sudo mkdir -p /mnt/ipod
131
+ sudo mount -t hfsplus -o ro /dev/sda2 /mnt/ipod
132
+ IPODSYNC_MOUNT=/mnt/ipod ipodsync list
133
+ sudo umount /mnt/ipod # before unplugging
134
+ ```
135
+
136
+ **Writing (`add` / `rm` / `cover`) on Linux** needs a read-write mount owned by
137
+ your user (so it can write files), and the GUID (below). Mount it like this:
138
+
139
+ ```bash
140
+ sudo mount -t hfsplus -o rw,uid=$(id -u),gid=$(id -g) /dev/sda2 /mnt/ipod
141
+ ```
142
+
143
+ Two catches:
144
+
145
+ - **The GUID.** Signing needs the device's FireWireGUID, which lives in a
146
+ `Device/SysInfo*` file — but those are HFS+-compressed and unreadable by the Linux
147
+ driver. ipodsync works around it by reading the GUID from the device's **USB serial**
148
+ automatically. If that fails, pass it yourself:
149
+ ```bash
150
+ udevadm info -q property -n /dev/sda | grep ID_SERIAL
151
+ # ID_SERIAL=Apple_iPod_0123456789ABCDEF-0:0
152
+ IPODSYNC_FIREWIRE_GUID=0123456789ABCDEF IPODSYNC_MOUNT=/mnt/ipod ipodsync add song.mp3
153
+ ```
154
+ - **Journaling.** If the mount comes up read-only, the volume is *journaled* HFS+;
155
+ disable the journal once, on a Mac: `diskutil disableJournal /Volumes/iPod`. Forcing
156
+ it with `-o force,rw` on a still-journaled volume can corrupt the filesystem — don't.
157
+
107
158
  ## How it works
108
159
 
109
160
  - **Transport** — mass storage: files are written under `iPod_Control/`.
@@ -72,6 +72,57 @@ The iPod is discovered under `/Volumes` (macOS) and `/media`, `/run/media`, `/mn
72
72
  (Linux) by the presence of `iPod_Control/`. To point at it explicitly, set
73
73
  `IPODSYNC_MOUNT=/path/to/mount`.
74
74
 
75
+ ## Linux: mounting the iPod
76
+
77
+ On macOS, Finder mounts the iPod automatically. A headless Linux box usually
78
+ doesn't — the device shows up as a disk but stays unmounted, so `ipodsync` reports
79
+ "iPod not found". Mount it yourself, then point `IPODSYNC_MOUNT` at it.
80
+
81
+ Find the iPod's partition (an `hfsplus` one, roughly the iPod's size):
82
+
83
+ ```bash
84
+ lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT # e.g. sda2 14.7G hfsplus
85
+ ```
86
+
87
+ For read commands (`list` / `export`), the quickest way is `--mount`, which mounts
88
+ the iPod for you via `udisksctl` (no root) and runs the command:
89
+
90
+ ```bash
91
+ ipodsync --mount list
92
+ ```
93
+
94
+ Or mount it yourself:
95
+
96
+ ```bash
97
+ sudo modprobe hfsplus
98
+ sudo mkdir -p /mnt/ipod
99
+ sudo mount -t hfsplus -o ro /dev/sda2 /mnt/ipod
100
+ IPODSYNC_MOUNT=/mnt/ipod ipodsync list
101
+ sudo umount /mnt/ipod # before unplugging
102
+ ```
103
+
104
+ **Writing (`add` / `rm` / `cover`) on Linux** needs a read-write mount owned by
105
+ your user (so it can write files), and the GUID (below). Mount it like this:
106
+
107
+ ```bash
108
+ sudo mount -t hfsplus -o rw,uid=$(id -u),gid=$(id -g) /dev/sda2 /mnt/ipod
109
+ ```
110
+
111
+ Two catches:
112
+
113
+ - **The GUID.** Signing needs the device's FireWireGUID, which lives in a
114
+ `Device/SysInfo*` file — but those are HFS+-compressed and unreadable by the Linux
115
+ driver. ipodsync works around it by reading the GUID from the device's **USB serial**
116
+ automatically. If that fails, pass it yourself:
117
+ ```bash
118
+ udevadm info -q property -n /dev/sda | grep ID_SERIAL
119
+ # ID_SERIAL=Apple_iPod_0123456789ABCDEF-0:0
120
+ IPODSYNC_FIREWIRE_GUID=0123456789ABCDEF IPODSYNC_MOUNT=/mnt/ipod ipodsync add song.mp3
121
+ ```
122
+ - **Journaling.** If the mount comes up read-only, the volume is *journaled* HFS+;
123
+ disable the journal once, on a Mac: `diskutil disableJournal /Volumes/iPod`. Forcing
124
+ it with `-o force,rw` on a still-journaled volume can corrupt the filesystem — don't.
125
+
75
126
  ## How it works
76
127
 
77
128
  - **Transport** — mass storage: files are written under `iPod_Control/`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ipodsync"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Upload music to an iPod nano 6G/7G without iTunes: SQLite library, hashAB signing, cover art (pure-Python)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -13,7 +13,7 @@ from .library import ItlpLibrary
13
13
  from .models import Track
14
14
  from .transport import IPod, IPodNotFound, find_ipod
15
15
 
16
- __version__ = "0.1.2"
16
+ __version__ = "0.1.3"
17
17
 
18
18
  __all__ = [
19
19
  "find_ipod",
@@ -14,6 +14,7 @@ or a sync will drop manually-added tracks.
14
14
  from __future__ import annotations
15
15
 
16
16
  import argparse
17
+ import os
17
18
  import shutil
18
19
  from datetime import datetime, timezone
19
20
  from pathlib import Path
@@ -77,7 +78,7 @@ def cmd_rm(ipod, args):
77
78
  shutil.copytree(itlp, backup)
78
79
  print(f".itlp backup: {backup}")
79
80
 
80
- guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo)
81
+ guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo, mount=ipod.root)
81
82
  lib = ItlpLibrary(itlp)
82
83
  music = ipod.music_dir if args.delete_file else None
83
84
  loc = lib.remove_track(args.pid, music_dir=music)
@@ -181,7 +182,7 @@ def cmd_add(ipod, args):
181
182
  shutil.copytree(itlp, backup)
182
183
  print(f".itlp backup: {backup}")
183
184
 
184
- guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo)
185
+ guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo, mount=ipod.root)
185
186
  location, abs_path = copy_audio_to_ipod(ipod, args.file)
186
187
  print(f"File copied: {location}")
187
188
 
@@ -264,6 +265,8 @@ def main() -> int:
264
265
  epilog="Set IPODSYNC_MOUNT=/path to point at the iPod explicitly. "
265
266
  "add/rm/cover back up the library before editing. Run "
266
267
  "`ipodsync <command> -h` for per-command help.")
268
+ ap.add_argument("--mount", action="store_true",
269
+ help="Linux: mount the iPod first via udisksctl (read-only)")
267
270
  sub = ap.add_subparsers(dest="cmd", required=True, metavar="<command>")
268
271
 
269
272
  sub.add_parser("list", help="show tracks on the iPod")
@@ -309,6 +312,16 @@ def main() -> int:
309
312
 
310
313
  args = ap.parse_args()
311
314
 
315
+ if args.mount:
316
+ from ipodsync.transport import mount_ipod
317
+ try:
318
+ mp = mount_ipod()
319
+ except IPodNotFound as e:
320
+ print(f"⚠️ {e}")
321
+ return 2
322
+ os.environ["IPODSYNC_MOUNT"] = mp
323
+ print(f"Mounted iPod at {mp}")
324
+
312
325
  # commands that don't require a ready iPod
313
326
  if args.cmd == "status":
314
327
  return cmd_status(args)
@@ -0,0 +1,118 @@
1
+ """Read the device's FirewireGuid — needed for the hashAB signature.
2
+
3
+ FirewireGuid is an 8-byte device identifier. It normally lives in
4
+ `iPod_Control/Device/SysInfoExtended` (XML plist) or `SysInfo` (text), as a hex
5
+ string like `0x000A2700XXXXXXXX` (16 hex chars).
6
+
7
+ On a Mac-formatted iPod mounted on Linux those files are often unreadable (HFS+
8
+ transparent compression / restrictive perms). As fallbacks we accept the GUID via
9
+ the `IPODSYNC_FIREWIRE_GUID` env var, and derive it from the device's USB serial
10
+ (e.g. udev `ID_SERIAL=Apple_iPod_0123456789ABCDEF-0:0`), which stays readable.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ ENV_GUID = "IPODSYNC_FIREWIRE_GUID"
21
+
22
+ # 0x + 16 hex, or a FireWireGUID key near 16 hex chars
23
+ _GUID_RE = re.compile(
24
+ r"(?:FireWireGUID|FirewireGuid)\D{0,40}?(?:0x)?([0-9A-Fa-f]{16})",
25
+ re.IGNORECASE | re.DOTALL,
26
+ )
27
+ _BARE_HEX_RE = re.compile(r"0x([0-9A-Fa-f]{16})")
28
+ _SERIAL_RE = re.compile(r"ID_SERIAL=\S*?([0-9A-Fa-f]{16})")
29
+
30
+
31
+ class FirewireGuidNotFound(RuntimeError):
32
+ pass
33
+
34
+
35
+ def _clean_hex(s: str) -> str | None:
36
+ s = s.strip().lower()
37
+ if s.startswith("0x"):
38
+ s = s[2:]
39
+ return s if re.fullmatch(r"[0-9a-f]{16}", s) else None
40
+
41
+
42
+ def _extract(text: str) -> str | None:
43
+ m = _GUID_RE.search(text) or _BARE_HEX_RE.search(text)
44
+ return m.group(1) if m else None
45
+
46
+
47
+ def _from_env() -> bytes | None:
48
+ v = os.environ.get(ENV_GUID)
49
+ if not v:
50
+ return None
51
+ h = _clean_hex(v)
52
+ if not h:
53
+ raise FirewireGuidNotFound(
54
+ f"{ENV_GUID}={v!r} is not 16 hex digits (e.g. 0123456789ABCDEF)")
55
+ return bytes.fromhex(h)
56
+
57
+
58
+ def _from_files(*paths: Path | None) -> bytes | None:
59
+ for p in paths:
60
+ if p is None:
61
+ continue
62
+ try:
63
+ if not p.exists():
64
+ continue
65
+ text = p.read_text(encoding="utf-8", errors="ignore")
66
+ except OSError:
67
+ continue # unreadable (e.g. HFS+ compression on Linux)
68
+ h = _extract(text)
69
+ if h:
70
+ return bytes.fromhex(h)
71
+ return None
72
+
73
+
74
+ def _run(cmd: list[str]) -> str:
75
+ try:
76
+ return subprocess.run(cmd, capture_output=True, text=True,
77
+ timeout=5).stdout.strip()
78
+ except (OSError, subprocess.SubprocessError):
79
+ return ""
80
+
81
+
82
+ def _from_usb_serial(mount: Path | None) -> bytes | None:
83
+ """Linux: derive the GUID from the iPod's USB serial (readable when the
84
+ HFS+ SysInfo* files are not)."""
85
+ if mount is None or not sys.platform.startswith("linux"):
86
+ return None
87
+ src = _run(["findmnt", "-no", "SOURCE", "--target", str(mount)])
88
+ devices = [src] if src else []
89
+ if src:
90
+ pk = _run(["lsblk", "-no", "pkname", src])
91
+ if pk:
92
+ devices.append(f"/dev/{pk}")
93
+ for dev in devices:
94
+ m = _SERIAL_RE.search(_run(["udevadm", "info", "-q", "property", "-n", dev]))
95
+ if m:
96
+ return bytes.fromhex(m.group(1))
97
+ return None
98
+
99
+
100
+ def read_firewire_guid(sysinfo_extended: Path, sysinfo: Path | None = None,
101
+ mount: Path | None = None) -> bytes:
102
+ """Return the FirewireGuid as 8 bytes (big-endian, as in the hex string).
103
+
104
+ Resolution order: `IPODSYNC_FIREWIRE_GUID` env → SysInfoExtended/SysInfo files
105
+ → the device's USB serial (Linux). `mount` is the iPod root (for the USB fallback).
106
+ """
107
+ for guid in (_from_env(), _from_files(sysinfo_extended, sysinfo),
108
+ _from_usb_serial(mount)):
109
+ if guid:
110
+ return guid
111
+ raise FirewireGuidNotFound(
112
+ "Could not read the device FireWireGUID.\n"
113
+ f" Tried {sysinfo_extended} / {sysinfo}"
114
+ + (" (unreadable — Mac-formatted HFS+ files often aren't readable on Linux)\n"
115
+ if not sys.platform == "darwin" else "\n")
116
+ + f" Fix: pass it via {ENV_GUID}=<16 hex digits>. Find it with:\n"
117
+ + " udevadm info -q property -n /dev/sdXN | grep ID_SERIAL (Linux)\n"
118
+ + " e.g. ID_SERIAL=Apple_iPod_0123456789ABCDEF-0:0 -> 0123456789ABCDEF")
@@ -16,6 +16,8 @@ removable volume when the terminal lacks Full Disk Access (TCC). We tell that st
16
16
  from __future__ import annotations
17
17
 
18
18
  import os
19
+ import re
20
+ import subprocess
19
21
  import sys
20
22
  import time
21
23
  from dataclasses import dataclass
@@ -140,10 +142,7 @@ def find_ipod(name_hint: str = "iPod") -> IPod:
140
142
  return ipod
141
143
  if status is Access.NO_ACCESS:
142
144
  raise IPodNotFound(NO_ACCESS_HINT.format(vols=", ".join(v.name for v in blocked)))
143
- raise IPodNotFound(
144
- "iPod not found. Connect the device (enable Disk Use), or set "
145
- "IPODSYNC_MOUNT=/path/to/mount."
146
- )
145
+ raise IPodNotFound(_not_found_message())
147
146
 
148
147
 
149
148
  def wait_for_ipod(timeout: float = 120, interval: float = 2,
@@ -163,3 +162,70 @@ def wait_for_ipod(timeout: float = 120, interval: float = 2,
163
162
  raise IPodNotFound(f"iPod did not become available within {timeout}s "
164
163
  f"(last state: {status.value})")
165
164
  time.sleep(interval)
165
+
166
+
167
+ # --- Linux: detect / mount an unmounted iPod block device -----------------------
168
+ def _run(cmd: list[str]) -> tuple[str, str]:
169
+ try:
170
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=8)
171
+ return p.stdout, p.stderr
172
+ except (OSError, subprocess.SubprocessError):
173
+ return "", ""
174
+
175
+
176
+ def find_ipod_block_device() -> dict | None:
177
+ """Linux: locate the iPod's HFS+ block device.
178
+
179
+ Lists hfsplus partitions and confirms each is an Apple iPod via udev
180
+ (lsblk vendor/model aren't always populated on the partition).
181
+ Returns {'path': '/dev/sdXN', 'mountpoint': str|None} or None.
182
+ """
183
+ if not sys.platform.startswith("linux"):
184
+ return None
185
+ out, _ = _run(["lsblk", "-rno", "PATH,FSTYPE,MOUNTPOINT"])
186
+ for line in out.splitlines():
187
+ f = line.split(None, 2)
188
+ if len(f) < 2 or f[1] != "hfsplus":
189
+ continue
190
+ path, mp = f[0], (f[2] if len(f) > 2 else "")
191
+ info, _ = _run(["udevadm", "info", "-q", "property", "-n", path])
192
+ low = info.lower()
193
+ if ("apple_ipod" in low or "id_model=ipod" in low
194
+ or "id_vendor=apple" in low or "scsi_vendor=apple" in low):
195
+ return {"path": path, "mountpoint": mp or None}
196
+ return None
197
+
198
+
199
+ def _not_found_message() -> str:
200
+ dev = find_ipod_block_device()
201
+ if dev and dev.get("path") and not dev.get("mountpoint"):
202
+ return (f"iPod detected at {dev['path']} but not mounted.\n"
203
+ f" Mount it (read-only, for list/export): ipodsync --mount <command>\n"
204
+ f" or manually: udisksctl mount -b {dev['path']}\n"
205
+ " For writing (add/rm) mount read-write with uid= — see the README "
206
+ "\"Linux: mounting the iPod\".")
207
+ return ("iPod not found. Connect the device (enable Disk Use on the iPod), "
208
+ "or set IPODSYNC_MOUNT=/path/to/mount.")
209
+
210
+
211
+ def mount_ipod() -> str:
212
+ """Mount the iPod via udisksctl (Linux, no root). Returns the mountpoint.
213
+
214
+ This is read-only for a journaled HFS+ volume — enough for list/export; for
215
+ add/rm mount read-write with uid= yourself (see the README).
216
+ """
217
+ if sys.platform == "darwin":
218
+ raise IPodNotFound("On macOS the iPod is mounted automatically by Finder.")
219
+ dev = find_ipod_block_device()
220
+ if not dev or not dev.get("path"):
221
+ raise IPodNotFound("No iPod block device found — is it connected?")
222
+ if dev.get("mountpoint"):
223
+ return dev["mountpoint"]
224
+ out, err = _run(["udisksctl", "mount", "-b", dev["path"]])
225
+ m = re.search(r"\bat\s+(\S+)", out)
226
+ if m:
227
+ return m.group(1).rstrip(".")
228
+ raise IPodNotFound(
229
+ f"Could not mount {dev['path']} via udisksctl"
230
+ + (f": {err.strip()}" if err.strip() else "")
231
+ + ". Mount it manually — see the README.")
@@ -1,50 +0,0 @@
1
- """Read the device's FirewireGuid — needed for the hashAB signature.
2
-
3
- FirewireGuid is an 8-byte device identifier. It lives in
4
- `iPod_Control/Device/SysInfoExtended` (XML plist) or `SysInfo` (text),
5
- as a hex string like `0x000A2700XXXXXXXX` (16 hex characters).
6
- """
7
- from __future__ import annotations
8
-
9
- import re
10
- from pathlib import Path
11
-
12
- # 0x + 16 hex, or a FireWireGUID key near 16 hex chars
13
- _GUID_RE = re.compile(
14
- r"(?:FireWireGUID|FirewireGuid)\D{0,40}?(?:0x)?([0-9A-Fa-f]{16})",
15
- re.IGNORECASE | re.DOTALL,
16
- )
17
- _BARE_HEX_RE = re.compile(r"0x([0-9A-Fa-f]{16})")
18
-
19
-
20
- class FirewireGuidNotFound(RuntimeError):
21
- pass
22
-
23
-
24
- def _extract(text: str) -> str | None:
25
- m = _GUID_RE.search(text)
26
- if m:
27
- return m.group(1)
28
- m = _BARE_HEX_RE.search(text)
29
- if m:
30
- return m.group(1)
31
- return None
32
-
33
-
34
- def read_firewire_guid(sysinfo_extended: Path, sysinfo: Path | None = None) -> bytes:
35
- """Return FirewireGuid as 8 bytes (big-endian, as in the hex string).
36
-
37
- Tries SysInfoExtended, then SysInfo. errors='ignore' — the files may
38
- contain binary tails.
39
- """
40
- for p in (sysinfo_extended, sysinfo):
41
- if p is None or not p.exists():
42
- continue
43
- text = p.read_text(encoding="utf-8", errors="ignore")
44
- hexs = _extract(text)
45
- if hexs:
46
- return bytes.fromhex(hexs)
47
- raise FirewireGuidNotFound(
48
- f"Could not find FireWireGUID in {sysinfo_extended}"
49
- + (f" / {sysinfo}" if sysinfo else "")
50
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes