ipodsync 0.1.4__tar.gz → 0.1.5__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 (41) hide show
  1. {ipodsync-0.1.4 → ipodsync-0.1.5}/PKG-INFO +18 -5
  2. {ipodsync-0.1.4 → ipodsync-0.1.5}/README.md +17 -4
  3. {ipodsync-0.1.4 → ipodsync-0.1.5}/pyproject.toml +1 -1
  4. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/__init__.py +1 -1
  5. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/cli.py +56 -40
  6. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/transport.py +23 -13
  7. {ipodsync-0.1.4 → ipodsync-0.1.5}/.github/workflows/ci.yml +0 -0
  8. {ipodsync-0.1.4 → ipodsync-0.1.5}/.github/workflows/publish.yml +0 -0
  9. {ipodsync-0.1.4 → ipodsync-0.1.5}/.gitignore +0 -0
  10. {ipodsync-0.1.4 → ipodsync-0.1.5}/LICENSE +0 -0
  11. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/__main__.py +0 -0
  12. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_art_templates.py +0 -0
  13. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_gen.py +0 -0
  14. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_gen2.py +0 -0
  15. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_gen_rt.py +0 -0
  16. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_py.py +0 -0
  17. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_tables.bin +0 -0
  18. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_hashab_tables.py +0 -0
  19. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/_lib_templates.py +0 -0
  20. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/artwork_writer.py +0 -0
  21. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/cbk.py +0 -0
  22. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/export.py +0 -0
  23. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/hashab.py +0 -0
  24. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/importer.py +0 -0
  25. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/library.py +0 -0
  26. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/models.py +0 -0
  27. {ipodsync-0.1.4 → ipodsync-0.1.5}/src/ipodsync/sysinfo.py +0 -0
  28. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/data/ArtworkDB +0 -0
  29. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/data/Locations.itdb +0 -0
  30. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/data/Locations.itdb.cbk +0 -0
  31. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/data/hashab-traces.json +0 -0
  32. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/data/hashab-vectors.json +0 -0
  33. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/test_artwork_writer.py +0 -0
  34. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/test_cbk.py +0 -0
  35. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/test_hashab.py +0 -0
  36. {ipodsync-0.1.4 → ipodsync-0.1.5}/tests/test_hashab_port.py +0 -0
  37. {ipodsync-0.1.4 → ipodsync-0.1.5}/tools/build_gen2.py +0 -0
  38. {ipodsync-0.1.4 → ipodsync-0.1.5}/tools/gen_art_templates.py +0 -0
  39. {ipodsync-0.1.4 → ipodsync-0.1.5}/tools/gen_hashab_tables.py +0 -0
  40. {ipodsync-0.1.4 → ipodsync-0.1.5}/tools/gen_lib_templates.py +0 -0
  41. {ipodsync-0.1.4 → ipodsync-0.1.5}/tools/transpile_hashab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipodsync
3
- Version: 0.1.4
3
+ Version: 0.1.5
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
@@ -98,8 +98,12 @@ ipodsync add "Song.mp3" --no-cover
98
98
  ipodsync export ~/Music/ipod --by-album
99
99
  ipodsync cover 123456789 --image cover.jpg
100
100
  ipodsync rm 123456789 --delete-file
101
+ ipodsync -b add "Song.mp3" # -b: back up the library first (off by default)
101
102
  ```
102
103
 
104
+ Writes don't back up the library by default — pass `-b` / `--backup` to snapshot
105
+ `iTunes Library.itlp` to `~/ipod-backups` before editing.
106
+
103
107
  The iPod is discovered under `/Volumes` (macOS) and `/media`, `/run/media`, `/mnt`
104
108
  (Linux) by the presence of `iPod_Control/`. To point at it explicitly, set
105
109
  `IPODSYNC_MOUNT=/path/to/mount`.
@@ -116,8 +120,8 @@ Find the iPod's partition (an `hfsplus` one, roughly the iPod's size):
116
120
  lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT # e.g. sda2 14.7G hfsplus
117
121
  ```
118
122
 
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:
123
+ For read commands (`list` / `export`), the quickest way is `--mount`: it detects the
124
+ device, mounts it read-only (asks for `sudo`), runs the command, then unmounts:
121
125
 
122
126
  ```bash
123
127
  ipodsync --mount list
@@ -133,11 +137,20 @@ IPODSYNC_MOUNT=/mnt/ipod ipodsync list
133
137
  sudo umount /mnt/ipod # before unplugging
134
138
  ```
135
139
 
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:
140
+ **Writing (`add` / `rm` / `cover`) on Linux.** `--mount` handles it in one command —
141
+ it detects the device, mounts HFS+ read-write owned by your user (asks for `sudo`),
142
+ runs the command, then unmounts:
143
+
144
+ ```bash
145
+ ipodsync --mount add song.mp3
146
+ ipodsync --mount -b add song.mp3 # -b: snapshot the library to ~/ipod-backups first
147
+ ```
148
+
149
+ Or mount it yourself and point `IPODSYNC_MOUNT` at it:
138
150
 
139
151
  ```bash
140
152
  sudo mount -t hfsplus -o rw,uid=$(id -u),gid=$(id -g) /dev/sda2 /mnt/ipod
153
+ IPODSYNC_MOUNT=/mnt/ipod ipodsync add song.mp3
141
154
  ```
142
155
 
143
156
  Two catches:
@@ -66,8 +66,12 @@ ipodsync add "Song.mp3" --no-cover
66
66
  ipodsync export ~/Music/ipod --by-album
67
67
  ipodsync cover 123456789 --image cover.jpg
68
68
  ipodsync rm 123456789 --delete-file
69
+ ipodsync -b add "Song.mp3" # -b: back up the library first (off by default)
69
70
  ```
70
71
 
72
+ Writes don't back up the library by default — pass `-b` / `--backup` to snapshot
73
+ `iTunes Library.itlp` to `~/ipod-backups` before editing.
74
+
71
75
  The iPod is discovered under `/Volumes` (macOS) and `/media`, `/run/media`, `/mnt`
72
76
  (Linux) by the presence of `iPod_Control/`. To point at it explicitly, set
73
77
  `IPODSYNC_MOUNT=/path/to/mount`.
@@ -84,8 +88,8 @@ Find the iPod's partition (an `hfsplus` one, roughly the iPod's size):
84
88
  lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT # e.g. sda2 14.7G hfsplus
85
89
  ```
86
90
 
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:
91
+ For read commands (`list` / `export`), the quickest way is `--mount`: it detects the
92
+ device, mounts it read-only (asks for `sudo`), runs the command, then unmounts:
89
93
 
90
94
  ```bash
91
95
  ipodsync --mount list
@@ -101,11 +105,20 @@ IPODSYNC_MOUNT=/mnt/ipod ipodsync list
101
105
  sudo umount /mnt/ipod # before unplugging
102
106
  ```
103
107
 
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:
108
+ **Writing (`add` / `rm` / `cover`) on Linux.** `--mount` handles it in one command —
109
+ it detects the device, mounts HFS+ read-write owned by your user (asks for `sudo`),
110
+ runs the command, then unmounts:
111
+
112
+ ```bash
113
+ ipodsync --mount add song.mp3
114
+ ipodsync --mount -b add song.mp3 # -b: snapshot the library to ~/ipod-backups first
115
+ ```
116
+
117
+ Or mount it yourself and point `IPODSYNC_MOUNT` at it:
106
118
 
107
119
  ```bash
108
120
  sudo mount -t hfsplus -o rw,uid=$(id -u),gid=$(id -g) /dev/sda2 /mnt/ipod
121
+ IPODSYNC_MOUNT=/mnt/ipod ipodsync add song.mp3
109
122
  ```
110
123
 
111
124
  Two catches:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ipodsync"
7
- version = "0.1.4"
7
+ version = "0.1.5"
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.4"
16
+ __version__ = "0.1.5"
17
17
 
18
18
  __all__ = [
19
19
  "find_ipod",
@@ -7,9 +7,9 @@
7
7
  ipodsync rm PID [--delete-file] # remove a track (+resign)
8
8
  ipodsync cover PID [--image IMG] # attach a cover to a track
9
9
 
10
- Export is read-only for the device. add/rm/cover write to the database (they back up .itlp).
11
- Browsing the iPod in Finder/Music is fine; just don't let Apple's software auto-sync it,
12
- or a sync will drop manually-added tracks.
10
+ Export is read-only. add/rm/cover write to the database; pass -b to back the library
11
+ up to ~/ipod-backups first. Browsing the iPod in Finder/Music is fine; just don't let
12
+ Apple's software auto-sync it, or a sync will drop manually-added tracks.
13
13
  """
14
14
  from __future__ import annotations
15
15
 
@@ -73,10 +73,7 @@ def cmd_export(ipod, args):
73
73
  def cmd_rm(ipod, args):
74
74
  itlp = ipod.itunes_dir / "iTunes Library.itlp"
75
75
  stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
76
- backup = Path.home() / "ipod-backups" / f"itlp-{stamp}"
77
- backup.parent.mkdir(parents=True, exist_ok=True)
78
- shutil.copytree(itlp, backup)
79
- print(f".itlp backup: {backup}")
76
+ backup = _backup_library(itlp, stamp) if args.backup else None
80
77
 
81
78
  guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo, mount=ipod.root)
82
79
  lib = ItlpLibrary(itlp)
@@ -84,21 +81,20 @@ def cmd_rm(ipod, args):
84
81
  loc = lib.remove_track(args.pid, music_dir=music)
85
82
  lib.resign(guid)
86
83
  lib.close()
87
- print(f"Removed track pid={args.pid} (file: {loc or '—'}"
88
- f"{', deleted' if args.delete_file else ''}). Eject iPod.")
89
- print(f"Rollback: rm -rf '{itlp}' && cp -r '{backup}' '{itlp}'")
84
+ print(f"Removed track pid {args.pid}"
85
+ f"{' (+ deleted the file)' if args.delete_file else ''}. Eject the iPod.")
86
+ if backup:
87
+ print(f" Undo: rm -rf '{itlp}' && cp -r '{backup}' '{itlp}'")
90
88
 
91
89
 
92
- def _edit_library(ipod, fn, *, label: str):
93
- """Back up .itlp, edit a copy via fn(lib), copy Library/Dynamic.itdb back.
90
+ def _edit_library(ipod, fn, *, label: str, backup: bool = False):
91
+ """Edit a /tmp copy via fn(lib), copy Library/Dynamic.itdb back.
94
92
 
95
93
  Playlist operations don't touch Locations.itdb, so we don't rebuild cbk.
96
94
  """
97
95
  itlp = ipod.itunes_dir / "iTunes Library.itlp"
98
96
  stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
99
- backup = Path.home() / "ipod-backups" / f"itlp-{stamp}"
100
- backup.parent.mkdir(parents=True, exist_ok=True)
101
- shutil.copytree(itlp, backup)
97
+ bak = _backup_library(itlp, stamp) if backup else None
102
98
  work = Path("/tmp") / f"itlp_{label}_{stamp}"
103
99
  shutil.copytree(itlp, work)
104
100
 
@@ -109,7 +105,7 @@ def _edit_library(ipod, fn, *, label: str):
109
105
  lib.close()
110
106
  for name in ("Library.itdb", "Dynamic.itdb"):
111
107
  shutil.copy2(work / name, itlp / name)
112
- print(f".itlp backup: {backup}. Eject iPod.")
108
+ print("✓ Done — eject the iPod." + (f" (backup: {bak.name})" if bak else ""))
113
109
  return result
114
110
 
115
111
 
@@ -123,24 +119,24 @@ def cmd_playlists(ipod, args):
123
119
 
124
120
  def cmd_pl_create(ipod, args):
125
121
  pid = _edit_library(ipod, lambda lib: lib.create_playlist(args.name, date=_mac_now()),
126
- label="plcreate")
122
+ label="plcreate", backup=args.backup)
127
123
  print(f"Created playlist '{args.name}' pid={pid}")
128
124
 
129
125
 
130
126
  def cmd_pl_add(ipod, args):
131
127
  _edit_library(ipod, lambda lib: [lib.add_to_playlist(args.playlist, t) for t in args.track],
132
- label="pladd")
128
+ label="pladd", backup=args.backup)
133
129
  print(f"Tracks added: {len(args.track)} → playlist {args.playlist}")
134
130
 
135
131
 
136
132
  def cmd_pl_rm(ipod, args):
137
133
  _edit_library(ipod, lambda lib: lib.remove_from_playlist(args.playlist, args.track),
138
- label="plrm")
134
+ label="plrm", backup=args.backup)
139
135
  print(f"Track {args.track} removed from playlist {args.playlist}")
140
136
 
141
137
 
142
138
  def cmd_pl_del(ipod, args):
143
- _edit_library(ipod, lambda lib: lib.delete_playlist(args.playlist), label="pldel")
139
+ _edit_library(ipod, lambda lib: lib.delete_playlist(args.playlist), label="pldel", backup=args.backup)
144
140
  print(f"Playlist {args.playlist} deleted")
145
141
 
146
142
 
@@ -177,30 +173,37 @@ def cmd_add(ipod, args):
177
173
  """Upload an MP3 to the iPod: audio -> Fxx/ (onto the device), .itlp -> edit a copy -> back."""
178
174
  itlp = ipod.itunes_dir / "iTunes Library.itlp"
179
175
  stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
180
- backup = Path.home() / "ipod-backups" / f"itlp-{stamp}"
181
- backup.parent.mkdir(parents=True, exist_ok=True)
182
- shutil.copytree(itlp, backup)
183
- print(f".itlp backup: {backup}")
176
+ backup = _backup_library(itlp, stamp) if args.backup else None
184
177
 
185
178
  guid = read_firewire_guid(ipod.sysinfo_extended, ipod.sysinfo, mount=ipod.root)
179
+ print("→ copying the track onto the iPod…")
186
180
  location, abs_path = copy_audio_to_ipod(ipod, args.file)
187
- print(f"File copied: {location}")
188
181
 
189
182
  work = Path("/tmp") / f"itlp_add_{stamp}"
190
183
  shutil.copytree(itlp, work)
191
184
  overrides = {"title": args.title, "artist": args.artist, "album": args.album}
192
185
  art_dir = None if args.no_cover else ipod.control / "Artwork"
193
- if art_dir is not None:
186
+ if art_dir is not None and args.backup:
194
187
  _backup_artwork(ipod, stamp)
195
188
  pid = add_mp3_to_library(work, location, args.file, abs_path.stat().st_size,
196
189
  guid, overrides=overrides, artwork_dir=art_dir)
197
190
  for name in ("Library.itdb", "Locations.itdb", "Dynamic.itdb",
198
191
  "Extras.itdb", "Locations.itdb.cbk"):
199
192
  shutil.copy2(work / name, itlp / name)
200
- print(f"Track added pid={pid}"
201
- f"{'' if args.no_cover else ' (cover attached if one was embedded)'}."
202
- " Eject iPod and check Songs.")
203
- print(f"Rollback: rm -rf '{itlp}' && cp -r '{backup}' '{itlp}'")
193
+ cover = "" if args.no_cover else " (+ cover art)"
194
+ print(f" Added \"{Path(args.file).name}\"{cover} · pid {pid}")
195
+ print(" Eject the iPod, then check Songs.")
196
+ if backup:
197
+ print(f" Undo: rm -rf '{itlp}' && cp -r '{backup}' '{itlp}'")
198
+
199
+
200
+ def _backup_library(itlp: Path, stamp: str) -> Path:
201
+ """Copy the whole .itlp to ~/ipod-backups (only when -b/--backup is given)."""
202
+ dst = Path.home() / "ipod-backups" / f"itlp-{stamp}"
203
+ dst.parent.mkdir(parents=True, exist_ok=True)
204
+ shutil.copytree(itlp, dst)
205
+ print(f"→ backed up the library ({dst.name})")
206
+ return dst
204
207
 
205
208
 
206
209
  def _backup_artwork(ipod, stamp: str) -> None:
@@ -208,8 +211,7 @@ def _backup_artwork(ipod, stamp: str) -> None:
208
211
  art = ipod.control / "Artwork"
209
212
  if art.exists():
210
213
  dst = Path.home() / "ipod-backups" / f"Artwork-{stamp}"
211
- shutil.copytree(art, dst)
212
- print(f"Artwork backup: {dst}")
214
+ shutil.copytree(art, dst) # backed up alongside the library
213
215
 
214
216
 
215
217
  def cmd_cover(ipod, args):
@@ -220,7 +222,8 @@ def cmd_cover(ipod, args):
220
222
 
221
223
  itlp = ipod.itunes_dir / "iTunes Library.itlp"
222
224
  stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
223
- _backup_artwork(ipod, stamp)
225
+ if args.backup:
226
+ _backup_artwork(ipod, stamp)
224
227
 
225
228
  if args.image:
226
229
  cover = Path(args.image).read_bytes()
@@ -252,8 +255,7 @@ def cmd_cover(ipod, args):
252
255
  finally:
253
256
  lib.close()
254
257
  shutil.copy2(work / "Library.itdb", itlp / "Library.itdb")
255
- print(f"Cover attached to pid={args.pid} (cache_id={image_id}, "
256
- f"album{'+' if album_pid else ' —'}). Eject iPod.")
258
+ print(f"Cover attached to pid {args.pid}. Eject the iPod and check the album art.")
257
259
 
258
260
 
259
261
  def main() -> int:
@@ -266,7 +268,10 @@ def main() -> int:
266
268
  "add/rm/cover back up the library before editing. Run "
267
269
  "`ipodsync <command> -h` for per-command help.")
268
270
  ap.add_argument("--mount", action="store_true",
269
- help="Linux: mount the iPod first via udisksctl (read-only)")
271
+ help="Linux: auto-mount the iPod (sudo) before the command, unmount after")
272
+ ap.add_argument("-b", "--backup", action="store_true",
273
+ help="back up the library to ~/ipod-backups before editing "
274
+ "(off by default)")
270
275
  sub = ap.add_subparsers(dest="cmd", required=True, metavar="<command>")
271
276
 
272
277
  sub.add_parser("list", help="show tracks on the iPod")
@@ -312,22 +317,33 @@ def main() -> int:
312
317
 
313
318
  args = ap.parse_args()
314
319
 
320
+ WRITE_CMDS = {"add", "rm", "cover", "pl-create", "pl-add", "pl-rm", "pl-del"}
321
+ mounted = None
315
322
  if args.mount:
316
323
  from ipodsync.transport import mount_ipod
317
324
  try:
318
- mp = mount_ipod()
325
+ mp, mounted_by_us = mount_ipod(rw=args.cmd in WRITE_CMDS)
319
326
  except IPodNotFound as e:
320
327
  print(f"⚠️ {e}")
321
328
  return 2
322
329
  os.environ["IPODSYNC_MOUNT"] = mp
323
- print(f"Mounted iPod at {mp}")
330
+ print(f" iPod mounted at {mp}")
331
+ mounted = mp if mounted_by_us else None
332
+
333
+ try:
334
+ return _dispatch(args)
335
+ finally:
336
+ if mounted:
337
+ from ipodsync.transport import umount_ipod
338
+ umount_ipod(mounted)
339
+ print(f"✓ iPod unmounted — safe to unplug")
324
340
 
325
- # commands that don't require a ready iPod
341
+
342
+ def _dispatch(args) -> int:
326
343
  if args.cmd == "status":
327
344
  return cmd_status(args)
328
345
  if args.cmd == "wait":
329
346
  return cmd_wait(args)
330
-
331
347
  try:
332
348
  ipod = find_ipod()
333
349
  except IPodNotFound as e:
@@ -208,11 +208,15 @@ def _not_found_message() -> str:
208
208
  "or set IPODSYNC_MOUNT=/path/to/mount.")
209
209
 
210
210
 
211
- def mount_ipod() -> str:
212
- """Mount the iPod via udisksctl (Linux, no root). Returns the mountpoint.
211
+ MOUNTPOINT = "/mnt/ipodsync"
213
212
 
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).
213
+
214
+ def mount_ipod(rw: bool = False) -> tuple[str, bool]:
215
+ """Mount the iPod on Linux via `sudo mount` (asks for a password if needed).
216
+
217
+ Auto-detects the current device node (which changes across re-plugs), mounts
218
+ HFS+ read-only (or force,rw,uid= for writes) at MOUNTPOINT. Returns
219
+ (mountpoint, mounted_by_us) — if it was already mounted, mounted_by_us is False.
216
220
  """
217
221
  if sys.platform == "darwin":
218
222
  raise IPodNotFound("On macOS the iPod is mounted automatically by Finder.")
@@ -220,12 +224,18 @@ def mount_ipod() -> str:
220
224
  if not dev or not dev.get("path"):
221
225
  raise IPodNotFound("No iPod block device found — is it connected?")
222
226
  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.")
227
+ return dev["mountpoint"], False
228
+ opts = f"force,rw,uid={os.getuid()},gid={os.getgid()},umask=022" if rw else "ro"
229
+ print(f"Mounting {dev['path']} at {MOUNTPOINT} ({opts})")
230
+ subprocess.run(["sudo", "mkdir", "-p", MOUNTPOINT]) # inherit stdio (prompt)
231
+ r = subprocess.run(["sudo", "mount", "-t", "hfsplus", "-o", opts,
232
+ dev["path"], MOUNTPOINT])
233
+ if r.returncode != 0:
234
+ raise IPodNotFound(
235
+ f"Failed to mount {dev['path']} (see the error above). "
236
+ "See the README \"Linux: mounting the iPod\".")
237
+ return MOUNTPOINT, True
238
+
239
+
240
+ def umount_ipod(mountpoint: str) -> None:
241
+ subprocess.run(["sudo", "umount", mountpoint])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes