discstation 0.1.7 → 0.1.9
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.
- package/install-macos.sh +2 -1
- package/package.json +1 -1
- package/src/discstation.py +62 -97
- package/src/discstation_host.py +39 -9
package/install-macos.sh
CHANGED
|
@@ -27,7 +27,8 @@ if ! command -v brew >/dev/null 2>&1; then
|
|
|
27
27
|
exit 1
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
|
-
brew install python ffmpeg cdrdao dvdauthor node yt-dlp xorriso mpv libdiscid handbrake
|
|
30
|
+
brew install python ffmpeg cdrdao dvdauthor node yt-dlp xorriso mpv libdiscid handbrake \
|
|
31
|
+
libdvdcss dvdbackup libcdio-paranoia
|
|
31
32
|
mkdir -p "$APP_DIR" "$VENV_DIR" "$CONFIG_DIR"
|
|
32
33
|
cp -R "$ROOT_DIR/src/." "$APP_DIR/"
|
|
33
34
|
python3 -m venv "$VENV_DIR"
|
package/package.json
CHANGED
package/src/discstation.py
CHANGED
|
@@ -1046,6 +1046,9 @@ def is_rewritable_disc(device):
|
|
|
1046
1046
|
"",
|
|
1047
1047
|
)
|
|
1048
1048
|
return "rw" in media_line
|
|
1049
|
+
# Non-Linux: the RW signal comes from the ID_CDROM_MEDIA_*_RW keys above,
|
|
1050
|
+
# which discstation_host.media_properties() tags from the drutil/diskutil
|
|
1051
|
+
# media type.
|
|
1049
1052
|
return False
|
|
1050
1053
|
|
|
1051
1054
|
|
|
@@ -1248,6 +1251,12 @@ def _classify_disc(device, props, failed, deadline):
|
|
|
1248
1251
|
print(f"Disc inspection failed: {e}")
|
|
1249
1252
|
kind = kind or "data_disc"
|
|
1250
1253
|
return _disc_info(True, kind, web_type=_web_type_for(kind, 0))
|
|
1254
|
+
if props.get("ID_CDROM_MEDIA_STATE") == "blank":
|
|
1255
|
+
try:
|
|
1256
|
+
cap = discstation_burn.disc_capacity_bytes(device) or 0
|
|
1257
|
+
except Exception:
|
|
1258
|
+
cap = 0
|
|
1259
|
+
return _disc_info(True, "blank", capacity_bytes=cap, web_type="BLANK")
|
|
1251
1260
|
return _disc_info(True, "unknown", web_type="UNKNOWN", failed_probes=failed)
|
|
1252
1261
|
|
|
1253
1262
|
# Fast path for a blank disc: cdrom_id reports this reliably in ~20ms, and
|
|
@@ -1452,8 +1461,6 @@ def disc_title(device):
|
|
|
1452
1461
|
if t:
|
|
1453
1462
|
return t
|
|
1454
1463
|
elif kind == "audio_cd":
|
|
1455
|
-
if discstation_host.system_name() == "darwin":
|
|
1456
|
-
return "Apple Music"
|
|
1457
1464
|
try:
|
|
1458
1465
|
toc = audio_cd_toc(device)
|
|
1459
1466
|
if toc and toc.get("track_count"):
|
|
@@ -1524,8 +1531,6 @@ def menu_items_for_disc(device):
|
|
|
1524
1531
|
had = discstation_burn.WORK.rglob("movie.mpg")
|
|
1525
1532
|
if any(True for _ in had):
|
|
1526
1533
|
items.append("BURN MPG")
|
|
1527
|
-
elif kind == "audio_cd" and discstation_host.system_name() == "darwin":
|
|
1528
|
-
items = ["APPLE MUSIC"]
|
|
1529
1534
|
elif kind in ("dvd_video", "audio_cd", "vcd", "svcd", "video_data", "data_disc", "data_cd"):
|
|
1530
1535
|
items = ["PLAY", "RIP"]
|
|
1531
1536
|
else:
|
|
@@ -1603,47 +1608,38 @@ def disc_video_files(mount_dir):
|
|
|
1603
1608
|
|
|
1604
1609
|
def audio_cd_toc(device):
|
|
1605
1610
|
if discstation_host.system_name() == "darwin":
|
|
1606
|
-
|
|
1611
|
+
paranoia = None
|
|
1612
|
+
for name in ("cd-paranoia", "cdparanoia"):
|
|
1613
|
+
try:
|
|
1614
|
+
paranoia = discstation_burn.tool(name)
|
|
1615
|
+
break
|
|
1616
|
+
except FileNotFoundError:
|
|
1617
|
+
continue
|
|
1618
|
+
if not paranoia:
|
|
1619
|
+
raise RuntimeError("cd-paranoia not installed (brew install libcdio-paranoia)")
|
|
1607
1620
|
result = subprocess.run(
|
|
1608
|
-
[
|
|
1609
|
-
"--device", discstation_host.cdrdao_device(device), str(toc_path)],
|
|
1621
|
+
[paranoia, "-Q", "-d", rip_device(device)],
|
|
1610
1622
|
capture_output=True, text=True, timeout=30,
|
|
1611
1623
|
)
|
|
1612
1624
|
text = ensure_text(result.stdout) + ensure_text(result.stderr)
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1625
|
+
# " 1. 18288 [04:03.63] 0 [00:00.00] no no 2"
|
|
1626
|
+
begins, lengths = [], []
|
|
1627
|
+
for line in text.splitlines():
|
|
1628
|
+
match = re.match(r"\s*(\d+)\.\s+(\d+)\s+\[[\d:.]+\]\s+(\d+)\s+\[", line)
|
|
1629
|
+
if match:
|
|
1630
|
+
lengths.append(int(match.group(2)))
|
|
1631
|
+
begins.append(int(match.group(3)))
|
|
1632
|
+
if not begins:
|
|
1633
|
+
detail = next((l.strip() for l in reversed(text.splitlines()) if l.strip()), "cd-paranoia -Q failed")
|
|
1617
1634
|
raise RuntimeError(f"Could not read macOS CD TOC: {detail[:100]}")
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
for block in re.split(r"(?m)^\s*//\s*Track\s+\d+\s*$", toc_text)[1:]:
|
|
1621
|
-
file_lines = re.findall(r"(?m)^\s*FILE\b.*$", block)
|
|
1622
|
-
file_times = re.findall(r"\d+:\d+:\d+", file_lines[-1]) if file_lines else []
|
|
1623
|
-
pregap_times = re.findall(r"(?m)^\s*SILENCE\s+(\d+:\d+:\d+)", block)
|
|
1624
|
-
if file_times:
|
|
1625
|
-
duration = _msf_frames(file_times[-1])
|
|
1626
|
-
duration += sum(_msf_frames(value) for value in pregap_times)
|
|
1627
|
-
durations.append(duration)
|
|
1628
|
-
if not durations:
|
|
1629
|
-
for line in text.splitlines():
|
|
1630
|
-
match = re.match(r"\s*(\d+)\s+AUDIO.*?\((\d+)\).*?\((\d+)\)", line)
|
|
1631
|
-
if match:
|
|
1632
|
-
durations.append(int(match.group(3)) - int(match.group(2)))
|
|
1633
|
-
if not durations:
|
|
1634
|
-
raise RuntimeError("Could not read macOS CD TOC")
|
|
1635
|
-
tracks = []
|
|
1636
|
-
position = 150
|
|
1637
|
-
for duration in durations:
|
|
1638
|
-
tracks.append(position)
|
|
1639
|
-
position += duration
|
|
1640
|
-
leadout = position
|
|
1635
|
+
tracks = [begin + 150 for begin in begins] # LBA -> MB frame offset
|
|
1636
|
+
leadout = begins[-1] + lengths[-1] + 150
|
|
1641
1637
|
return {
|
|
1642
1638
|
"first_track": 1,
|
|
1643
|
-
"track_count": len(
|
|
1639
|
+
"track_count": len(tracks),
|
|
1644
1640
|
"leadout": leadout,
|
|
1645
1641
|
"tracks": tracks,
|
|
1646
|
-
"toc": "+".join(map(str, [1, len(
|
|
1642
|
+
"toc": "+".join(map(str, [1, len(tracks), leadout, *tracks])),
|
|
1647
1643
|
}
|
|
1648
1644
|
if _libdiscid is not None:
|
|
1649
1645
|
try:
|
|
@@ -1689,11 +1685,6 @@ def audio_cd_toc(device):
|
|
|
1689
1685
|
}
|
|
1690
1686
|
|
|
1691
1687
|
|
|
1692
|
-
def _msf_frames(value):
|
|
1693
|
-
minutes, seconds, frames = (int(part) for part in value.split(":"))
|
|
1694
|
-
return (minutes * 60 + seconds) * 75 + frames
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
1688
|
def audio_cd_chapters(device):
|
|
1698
1689
|
if discstation_host.system_name() == "darwin":
|
|
1699
1690
|
toc = audio_cd_toc(device)
|
|
@@ -2310,7 +2301,19 @@ def iter_process_events(proc, idle_seconds=1.0, ser=None):
|
|
|
2310
2301
|
reader.join(timeout=1)
|
|
2311
2302
|
|
|
2312
2303
|
|
|
2304
|
+
def rip_device(device):
|
|
2305
|
+
"""The node a ripper should read. macOS libdvdread/HandBrake/cd-paranoia
|
|
2306
|
+
want the raw char node (/dev/rdiskN); Linux and others use `device` as-is."""
|
|
2307
|
+
if device and discstation_host.system_name() == "darwin":
|
|
2308
|
+
name = Path(device).name
|
|
2309
|
+
if name.startswith("disk"):
|
|
2310
|
+
return f"/dev/r{name}"
|
|
2311
|
+
return device
|
|
2312
|
+
|
|
2313
|
+
|
|
2313
2314
|
def device_size_bytes(device):
|
|
2315
|
+
if discstation_host.system_name() != "linux":
|
|
2316
|
+
return discstation_host.media_capacity_bytes(device) or 0
|
|
2314
2317
|
result = run_probe(["blockdev", "--getsize64", device], timeout=3)
|
|
2315
2318
|
try:
|
|
2316
2319
|
return int(ensure_text(result.stdout).strip() or "0")
|
|
@@ -3074,8 +3077,6 @@ def play_flow(ser):
|
|
|
3074
3077
|
kind = disc_kind(device)
|
|
3075
3078
|
print(f"Disc type: {kind}")
|
|
3076
3079
|
|
|
3077
|
-
if kind == "audio_cd" and discstation_host.system_name() == "darwin":
|
|
3078
|
-
raise RuntimeError("Apple Music handles audio CD playback on macOS")
|
|
3079
3080
|
if not shutil.which("mpv"):
|
|
3080
3081
|
raise RuntimeError("mpv not found")
|
|
3081
3082
|
|
|
@@ -3116,7 +3117,7 @@ def play_flow(ser):
|
|
|
3116
3117
|
"--input-ipc-server=" + MPV_SOCKET,
|
|
3117
3118
|
"--force-window=no",
|
|
3118
3119
|
"--idle=no",
|
|
3119
|
-
"--cdrom-device=" + device,
|
|
3120
|
+
"--cdrom-device=" + rip_device(device),
|
|
3120
3121
|
"--cdda-cdtext=yes",
|
|
3121
3122
|
"cdda://",
|
|
3122
3123
|
]
|
|
@@ -3285,8 +3286,6 @@ def rip_flow(ser, artist_hint=None, album_hint=None):
|
|
|
3285
3286
|
device = discstation_burn.disc_device()
|
|
3286
3287
|
kind = disc_kind(device)
|
|
3287
3288
|
|
|
3288
|
-
if kind == "audio_cd" and discstation_host.system_name() == "darwin":
|
|
3289
|
-
raise RuntimeError("Apple Music handles audio CD ripping on macOS")
|
|
3290
3289
|
if kind == "audio_cd":
|
|
3291
3290
|
rip_audio_cd(ser, device, artist_hint, album_hint)
|
|
3292
3291
|
return
|
|
@@ -3295,13 +3294,14 @@ def rip_flow(ser, artist_hint=None, album_hint=None):
|
|
|
3295
3294
|
rip_video_disc(ser, device, kind)
|
|
3296
3295
|
return
|
|
3297
3296
|
|
|
3298
|
-
if kind == "dvd_video" and discstation_host.system_name() == "darwin":
|
|
3299
|
-
rip_dvd_video_macos(ser, device)
|
|
3300
|
-
return
|
|
3301
|
-
|
|
3302
3297
|
if kind != "dvd_video":
|
|
3303
3298
|
raise RuntimeError(f"Unsupported disc: {kind}")
|
|
3304
3299
|
|
|
3300
|
+
# libdvdread / HandBrake / dvdbackup want the raw node on macOS and the
|
|
3301
|
+
# auto-mounted UDF/ISO volume released first (no-ops on Linux).
|
|
3302
|
+
device = rip_device(device)
|
|
3303
|
+
discstation_host.unmount_device(device)
|
|
3304
|
+
|
|
3305
3305
|
out_dir = RIP_ROOT / time.strftime("rip_%Y%m%d_%H%M%S")
|
|
3306
3306
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
3307
3307
|
|
|
@@ -3395,45 +3395,6 @@ def remux_or_copy_video(src, dest):
|
|
|
3395
3395
|
shutil.copy2(src, dest.with_suffix(src.suffix.lower()))
|
|
3396
3396
|
|
|
3397
3397
|
|
|
3398
|
-
def rip_dvd_video_macos(ser, device):
|
|
3399
|
-
out_dir = RIP_ROOT / f"dvd_video_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
3400
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
3401
|
-
send(ser, "STATUS:Ripping DVD")
|
|
3402
|
-
send(ser, "INFO:Copying VIDEO_TS")
|
|
3403
|
-
send(ser, "PROGRESS:0%")
|
|
3404
|
-
|
|
3405
|
-
with mounted_disc(device) as mount_dir:
|
|
3406
|
-
source_dir = mount_dir / "VIDEO_TS"
|
|
3407
|
-
if not source_dir.is_dir():
|
|
3408
|
-
raise RuntimeError("VIDEO_TS directory not found")
|
|
3409
|
-
files = sorted(path for path in source_dir.rglob("*") if path.is_file())
|
|
3410
|
-
total_bytes = sum(path.stat().st_size for path in files)
|
|
3411
|
-
copied_bytes = 0
|
|
3412
|
-
for source in files:
|
|
3413
|
-
relative = source.relative_to(source_dir)
|
|
3414
|
-
destination = out_dir / "VIDEO_TS" / relative
|
|
3415
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
3416
|
-
span = (source.stat().st_size / total_bytes * 100) if total_bytes else 0
|
|
3417
|
-
discstation_burn.copy_with_keepalive(
|
|
3418
|
-
ser,
|
|
3419
|
-
source,
|
|
3420
|
-
destination,
|
|
3421
|
-
base_pct=(copied_bytes / total_bytes * 100) if total_bytes else 0,
|
|
3422
|
-
pct_span=span,
|
|
3423
|
-
)
|
|
3424
|
-
copied_bytes += source.stat().st_size
|
|
3425
|
-
|
|
3426
|
-
(out_dir / "disc_info.json").write_text(
|
|
3427
|
-
json.dumps({"kind": "dvd_video", "files": [str(path.relative_to(out_dir)) for path in (out_dir / "VIDEO_TS").rglob("*") if path.is_file()]}, indent=2),
|
|
3428
|
-
)
|
|
3429
|
-
safe_send(ser, "PROGRESS:100%")
|
|
3430
|
-
safe_send(ser, "DONE:Rip complete!")
|
|
3431
|
-
print(f"DVD rip complete: {out_dir}")
|
|
3432
|
-
out_dir = _finalize_video_rip(ser, out_dir, device, "dvd_video")
|
|
3433
|
-
chown_to_sudo_user(out_dir)
|
|
3434
|
-
time.sleep(3)
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
3398
|
def rip_video_disc(ser, device, kind):
|
|
3438
3399
|
out_dir = RIP_ROOT / f"{kind}_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
3439
3400
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -3468,10 +3429,17 @@ def _rip_audio_cd_macos(ser, device, chapters, metadata, cover_path, out_dir):
|
|
|
3468
3429
|
raise RuntimeError("No audio CD tracks found")
|
|
3469
3430
|
wav_dir = out_dir / ".wav"
|
|
3470
3431
|
wav_dir.mkdir(parents=True, exist_ok=True)
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3432
|
+
paranoia = None
|
|
3433
|
+
for name in ("cd-paranoia", "cdparanoia"):
|
|
3434
|
+
try:
|
|
3435
|
+
paranoia = discstation_burn.tool(name)
|
|
3436
|
+
break
|
|
3437
|
+
except FileNotFoundError:
|
|
3438
|
+
continue
|
|
3439
|
+
if not paranoia:
|
|
3440
|
+
raise RuntimeError("cd-paranoia not installed (brew install libcdio-paranoia)")
|
|
3441
|
+
# -B batch mode writes track01.cdda.wav, track02.cdda.wav, ... in cwd.
|
|
3442
|
+
command = [paranoia, "-B", "-d", rip_device(device), "1-"]
|
|
3475
3443
|
send(ser, "STATUS:RIPPING AUDIO CD")
|
|
3476
3444
|
send(ser, "PROGRESS:0%")
|
|
3477
3445
|
proc = subprocess.Popen(
|
|
@@ -3493,7 +3461,7 @@ def _rip_audio_cd_macos(ser, device, chapters, metadata, cover_path, out_dir):
|
|
|
3493
3461
|
raise
|
|
3494
3462
|
proc.wait()
|
|
3495
3463
|
if proc.returncode != 0:
|
|
3496
|
-
detail = next((line.strip() for line in reversed(output) if line.strip()), "
|
|
3464
|
+
detail = next((line.strip() for line in reversed(output) if line.strip()), "cd-paranoia failed")
|
|
3497
3465
|
raise RuntimeError(f"Audio CD rip failed: {detail[:80]}")
|
|
3498
3466
|
|
|
3499
3467
|
wav_files = sorted(wav_dir.glob("*.wav"))
|
|
@@ -3873,9 +3841,6 @@ def station_loop(ser, url, artist_hint=None, album_hint=None):
|
|
|
3873
3841
|
_last_burn_result = "Burn complete"
|
|
3874
3842
|
elif mode == "PLAY":
|
|
3875
3843
|
play_flow(ser)
|
|
3876
|
-
elif mode == "APPLE MUSIC":
|
|
3877
|
-
safe_send(ser, "STATUS:Apple Music handles this audio CD")
|
|
3878
|
-
time.sleep(2)
|
|
3879
3844
|
elif mode == "RIP":
|
|
3880
3845
|
rip_flow(ser, artist_hint, album_hint)
|
|
3881
3846
|
_last_burn_result = "Rip complete"
|
package/src/discstation_host.py
CHANGED
|
@@ -163,6 +163,19 @@ def drive_status():
|
|
|
163
163
|
return "unknown"
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def _tag_rewritable(props, media_type):
|
|
167
|
+
"""Set the ID_CDROM_MEDIA_*_RW key that discstation.is_rewritable_disc reads,
|
|
168
|
+
from a lowercased media-type string (drutil 'Type:' or diskutil 'Optical
|
|
169
|
+
Media Type'). ponytail: substring match — same heuristic as the rest of this
|
|
170
|
+
module."""
|
|
171
|
+
if "cd-rw" in media_type or "cdrw" in media_type:
|
|
172
|
+
props["ID_CDROM_MEDIA_CD_RW"] = "1"
|
|
173
|
+
elif "dvd-rw" in media_type or "dvd-ram" in media_type:
|
|
174
|
+
props["ID_CDROM_MEDIA_DVD_RW"] = "1"
|
|
175
|
+
elif "dvd+rw" in media_type or "bd-re" in media_type:
|
|
176
|
+
props["ID_CDROM_MEDIA_DVD_PLUS_RW"] = "1"
|
|
177
|
+
|
|
178
|
+
|
|
166
179
|
def _drutil_media_properties():
|
|
167
180
|
result = subprocess.run(["/usr/bin/drutil", "status"], capture_output=True, text=True, check=False, timeout=3)
|
|
168
181
|
text = result.stdout + result.stderr
|
|
@@ -170,12 +183,16 @@ def _drutil_media_properties():
|
|
|
170
183
|
if result.returncode != 0 or "no media" in lowered:
|
|
171
184
|
return {}
|
|
172
185
|
props = {"ID_CDROM": "1", "ID_CDROM_MEDIA": "1"}
|
|
173
|
-
|
|
186
|
+
blank = "space used:" in lowered and "00:00:00" in lowered.split("space used:", 1)[1][:24]
|
|
187
|
+
if blank:
|
|
174
188
|
props["ID_CDROM_MEDIA_STATE"] = "blank"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
match = re.search(r"type:\s*([a-z0-9+\-]+)", lowered)
|
|
190
|
+
media_type = match.group(1) if match else ""
|
|
191
|
+
if media_type.startswith("dvd") or media_type.startswith("bd"):
|
|
178
192
|
props["ID_CDROM_MEDIA_TYPE"] = "dvd"
|
|
193
|
+
elif media_type.startswith("cd") and not blank and "-r" not in media_type:
|
|
194
|
+
props["ID_CDROM_MEDIA_TYPE"] = "audio"
|
|
195
|
+
_tag_rewritable(props, media_type)
|
|
179
196
|
return props
|
|
180
197
|
|
|
181
198
|
|
|
@@ -187,7 +204,7 @@ def media_properties(device):
|
|
|
187
204
|
except subprocess.TimeoutExpired:
|
|
188
205
|
return _drutil_media_properties()
|
|
189
206
|
if disk.returncode != 0:
|
|
190
|
-
return
|
|
207
|
+
return _drutil_media_properties()
|
|
191
208
|
fields = {}
|
|
192
209
|
for line in disk.stdout.splitlines():
|
|
193
210
|
if ":" in line:
|
|
@@ -195,7 +212,7 @@ def media_properties(device):
|
|
|
195
212
|
fields[key.strip()] = value.strip()
|
|
196
213
|
optical = fields.get("Optical Media Type", "")
|
|
197
214
|
if not optical and not fields.get("Device / Media Name"):
|
|
198
|
-
return
|
|
215
|
+
return _drutil_media_properties()
|
|
199
216
|
props = {"ID_CDROM": "1", "ID_CDROM_MEDIA": "1"}
|
|
200
217
|
label = fields.get("Volume Name", "")
|
|
201
218
|
filesystem = fields.get("Type (Bundle)") or fields.get("File System Personality")
|
|
@@ -223,8 +240,9 @@ def media_properties(device):
|
|
|
223
240
|
optical = optical.lower()
|
|
224
241
|
if "dvd+r dl" in optical or "dvd-r dl" in optical:
|
|
225
242
|
props["ID_CDROM_MEDIA_DVD_PLUS_R_DL"] = "1"
|
|
226
|
-
elif "dvd+r" in optical or "dvd-r" in optical:
|
|
243
|
+
elif ("dvd+r" in optical or "dvd-r" in optical) and "rw" not in optical:
|
|
227
244
|
props["ID_CDROM_MEDIA_DVD_PLUS_R"] = "1"
|
|
245
|
+
_tag_rewritable(props, optical)
|
|
228
246
|
return props
|
|
229
247
|
return {}
|
|
230
248
|
|
|
@@ -335,8 +353,20 @@ def cdrdao_device(device):
|
|
|
335
353
|
|
|
336
354
|
|
|
337
355
|
def unmount_device(device):
|
|
338
|
-
"""
|
|
339
|
-
|
|
356
|
+
"""Release an auto-mounted optical volume before handing the raw device to a
|
|
357
|
+
writer or ripper. Media stays loaded."""
|
|
358
|
+
if not device:
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
if system_name() == "darwin":
|
|
362
|
+
disk = re.sub(r"^/dev/r", "/dev/", device) # diskutil wants the block node
|
|
363
|
+
subprocess.run(
|
|
364
|
+
["/usr/sbin/diskutil", "unmountDisk", "force", disk],
|
|
365
|
+
capture_output=True, text=True, check=False, timeout=30,
|
|
366
|
+
)
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
if system_name() != "linux":
|
|
340
370
|
return True
|
|
341
371
|
|
|
342
372
|
udisksctl = shutil.which("udisksctl")
|