discstation 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discstation",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs"
@@ -2933,15 +2933,16 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
2933
2933
  pass
2934
2934
 
2935
2935
  env = os.environ.copy()
2936
- if "DISPLAY" not in env:
2937
- env["DISPLAY"] = ":0"
2938
- try:
2939
- uid = os.getuid()
2940
- home_xauth = Path.home() / ".Xauthority"
2941
- env.setdefault("XAUTHORITY", str(home_xauth) if home_xauth.exists() else f"/run/user/{uid}/.Xauthority")
2942
- env.setdefault("XDG_RUNTIME_DIR", f"/run/user/{uid}")
2943
- except Exception:
2944
- pass
2936
+ if discstation_host.system_name() == "linux":
2937
+ if "DISPLAY" not in env:
2938
+ env["DISPLAY"] = ":0"
2939
+ try:
2940
+ uid = os.getuid()
2941
+ home_xauth = Path.home() / ".Xauthority"
2942
+ env.setdefault("XAUTHORITY", str(home_xauth) if home_xauth.exists() else f"/run/user/{uid}/.Xauthority")
2943
+ env.setdefault("XDG_RUNTIME_DIR", f"/run/user/{uid}")
2944
+ except Exception:
2945
+ pass
2945
2946
 
2946
2947
  proc = subprocess.Popen(run_as_desktop_user(cmd), env=env)
2947
2948
 
@@ -3082,23 +3083,36 @@ def play_flow(ser):
3082
3083
 
3083
3084
  if kind == "dvd_video":
3084
3085
  if discstation_host.system_name() == "darwin":
3085
- with mounted_disc(device) as mount_dir:
3086
- video_ts = mount_dir / "VIDEO_TS"
3087
- files = sorted(
3088
- path for path in video_ts.glob("VTS_01_*.VOB")
3089
- if re.search(r"_\d+\.VOB$", path.name, re.IGNORECASE)
3090
- and not path.name.upper().endswith("_0.VOB")
3091
- )
3092
- if not files:
3093
- raise RuntimeError("No playable DVD title found")
3086
+ try:
3094
3087
  cmd = [
3095
3088
  "mpv",
3096
3089
  "--input-ipc-server=" + MPV_SOCKET,
3097
3090
  "--force-window=yes",
3098
3091
  "--idle=no",
3099
- *[str(path) for path in files],
3092
+ "--dvd-device=" + rip_device(device),
3093
+ "dvdnav://",
3100
3094
  ]
3101
3095
  _run_mpv(ser, cmd, "Playing DVD", kind)
3096
+ except RuntimeError:
3097
+ # libdvdnav couldn't open the disc — fall back to playing the
3098
+ # main title's VOBs off the mounted volume (no menus).
3099
+ with mounted_disc(device) as mount_dir:
3100
+ video_ts = mount_dir / "VIDEO_TS"
3101
+ files = sorted(
3102
+ path for path in video_ts.glob("VTS_01_*.VOB")
3103
+ if re.search(r"_\d+\.VOB$", path.name, re.IGNORECASE)
3104
+ and not path.name.upper().endswith("_0.VOB")
3105
+ )
3106
+ if not files:
3107
+ raise RuntimeError("No playable DVD title found")
3108
+ cmd = [
3109
+ "mpv",
3110
+ "--input-ipc-server=" + MPV_SOCKET,
3111
+ "--force-window=yes",
3112
+ "--idle=no",
3113
+ *[str(path) for path in files],
3114
+ ]
3115
+ _run_mpv(ser, cmd, "Playing DVD", kind)
3102
3116
  else:
3103
3117
  cmd = [
3104
3118
  "mpv",
@@ -3880,10 +3894,16 @@ def check_pidfile():
3880
3894
  old_pid = int(f.read().strip())
3881
3895
  try:
3882
3896
  os.kill(old_pid, 0)
3883
- with open(f"/proc/{old_pid}/cmdline") as f:
3884
- if "discstation" in f.read():
3885
- print(f"Already running (PID {old_pid}), exiting")
3886
- sys.exit(0)
3897
+ if sys.platform == "linux":
3898
+ with open(f"/proc/{old_pid}/cmdline") as f:
3899
+ alive = "discstation" in f.read()
3900
+ else:
3901
+ ps = subprocess.run(["ps", "-p", str(old_pid), "-o", "command="],
3902
+ capture_output=True, text=True)
3903
+ alive = "discstation" in ps.stdout
3904
+ if alive:
3905
+ print(f"Already running (PID {old_pid}), exiting")
3906
+ sys.exit(0)
3887
3907
  except (OSError, IOError):
3888
3908
  pass
3889
3909
  except (ValueError, OSError):
@@ -121,6 +121,12 @@ def reset_drive(device=None):
121
121
  """Power-cycle the USB optical enclosure by toggling its sysfs 'authorized'
122
122
  flag (or via DISCSTATION_USB_RESET_CMD). Best-effort; returns True on a
123
123
  completed toggle/hook, False otherwise."""
124
+ if discstation_host.system_name() == "darwin":
125
+ # ponytail: no USB re-enumeration on macOS; an eject/reload is the only
126
+ # soft reset available and it drops whatever disc is loaded.
127
+ for cmd in (["/usr/bin/drutil", "eject"], ["/usr/bin/drutil", "tray", "close"]):
128
+ subprocess.run(cmd, capture_output=True, timeout=15, check=False)
129
+ return True
124
130
  if discstation_host.system_name() != "linux":
125
131
  return False
126
132
  if not device:
@@ -1251,6 +1257,38 @@ def _run_growisofs(ser, growisofs_cmd, log_path, device=None):
1251
1257
  print(f"Disc eject skipped: {e}")
1252
1258
 
1253
1259
 
1260
+ def _run_hdiutil_burn(ser, image_path, device=None):
1261
+ """Burn a pre-built ISO on macOS via `hdiutil burn -puppetstrings`, streaming
1262
+ its PERCENT: lines to the ESP32."""
1263
+ send(ser, "STATUS:Burning image...")
1264
+ send(ser, "PROGRESS:0%")
1265
+ cmd = discstation_host.iso_burn_command(device or disc_device(), image_path)
1266
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
1267
+ out_lines = []
1268
+ last_pct = -1
1269
+ try:
1270
+ for line in iter_proc_or_cancel(proc, ser):
1271
+ out_lines.append(line)
1272
+ m = re.search(r"PERCENT:([\d.]+)", line)
1273
+ if m:
1274
+ pct = int(float(m.group(1)))
1275
+ if 0 <= pct <= 100 and pct != last_pct:
1276
+ last_pct = pct
1277
+ send(ser, f"PROGRESS:{min(pct, 99)}%")
1278
+ except (KeyboardInterrupt, SystemExit):
1279
+ stop_process(proc)
1280
+ raise
1281
+ rc = proc.wait()
1282
+ if rc != 0:
1283
+ detail = next((l for l in reversed(out_lines) if l.strip()), "hdiutil burn failed")
1284
+ raise RuntimeError(f"Disc burn failed: {detail[:120]}")
1285
+ safe_send(ser, "PROGRESS:100%")
1286
+ try:
1287
+ discstation_host.eject_device(device or disc_device())
1288
+ except Exception as e:
1289
+ print(f"Disc eject skipped: {e}")
1290
+
1291
+
1254
1292
  def burn(ser, dvd_dir, disc_label, speed=None, is_dual_layer=False):
1255
1293
  if discstation_host.system_name() != "linux":
1256
1294
  WORK.mkdir(parents=True, exist_ok=True)
@@ -1403,8 +1441,15 @@ def burn_audio_cd(ser, audio_files, disc_label, speed=None):
1403
1441
  # The cooked generic-mmc writer does NOT lay down the CD-TEXT lead-in on most
1404
1442
  # ATAPI drives; the raw writer does. Override with DISCSTATION_CDRDAO_DRIVER
1405
1443
  # (set it empty to let cdrdao auto-pick).
1444
+ try:
1445
+ cdrdao_write_dev = discstation_host.cdrdao_device(disc_device())
1446
+ except RuntimeError:
1447
+ if discstation_host.system_name() == "darwin":
1448
+ raise RuntimeError("Audio CD burning is not supported on this Mac "
1449
+ "(cdrdao cannot access the optical drive)")
1450
+ raise
1406
1451
  cdrdao_cmd = [tool('cdrdao'), 'write', '--buffers', '64',
1407
- '--device', discstation_host.cdrdao_device(disc_device())]
1452
+ '--device', cdrdao_write_dev]
1408
1453
  driver = os.environ.get("DISCSTATION_CDRDAO_DRIVER", "generic-mmc-raw")
1409
1454
  if driver:
1410
1455
  cdrdao_cmd += ['--driver', driver]
@@ -1454,6 +1499,9 @@ def burn_audio_cd(ser, audio_files, disc_label, speed=None):
1454
1499
 
1455
1500
  def burn_iso(ser, iso_path, speed=None, is_dual_layer=False):
1456
1501
  """Burn a pre-built ISO directly to disc — no filesystem building."""
1502
+ if discstation_host.system_name() == "darwin":
1503
+ _run_hdiutil_burn(ser, iso_path)
1504
+ return
1457
1505
  if discstation_host.system_name() != "linux":
1458
1506
  send(ser, "STATUS:Burning image...")
1459
1507
  _run_growisofs(ser, discstation_host.iso_burn_command(disc_device(), iso_path), iso_path.parent / "discstation-burn.log")
@@ -1493,8 +1541,9 @@ def remux_and_author(ser, mpg, disc_label, disc_capacity, dvd_aspect=None):
1493
1541
  send(ser, f"INFO:Burning {mpg.parent.name}")
1494
1542
 
1495
1543
  send(ser, "STATUS:Remuxing to fix timestamps...")
1496
- v_es = Path(f"/tmp/video_{os.getpid()}.m2v")
1497
- a_es = Path(f"/tmp/audio_{os.getpid()}.ac3")
1544
+ WORK.mkdir(parents=True, exist_ok=True)
1545
+ v_es = WORK / f"video_{os.getpid()}.m2v"
1546
+ a_es = WORK / f"audio_{os.getpid()}.ac3"
1498
1547
  fixed = mpg.parent / "movie_fixed.mpg"
1499
1548
  if fixed.exists():
1500
1549
  fixed.unlink()
@@ -295,12 +295,16 @@ def can_use_linux_optical_backend():
295
295
 
296
296
  def build_data_image(source_paths, output_path, label, video=False):
297
297
  system = system_name()
298
- if system == "darwin":
299
- command = [tool("hdiutil"), "makehybrid", "-o", str(output_path), "-iso", "-joliet", "-udf"]
300
- command += ["-default-volume-name", label, *[str(path) for path in source_paths]]
301
- elif system == "windows":
302
- command = [tool("xorriso"), "-as", "mkisofs", "-iso-level", "3", "-J", "-R", "-V", label, "-o", str(output_path)]
303
- command += [str(path) for path in source_paths]
298
+ if system in ("darwin", "windows"):
299
+ # xorriso's mkisofs emulation is the same lineage as Linux's
300
+ # genisoimage/growisofs; -dvd-video gives a set-top-compatible
301
+ # VIDEO_TS layout (the arg is the dir *containing* VIDEO_TS).
302
+ command = [tool("xorriso"), "-as", "mkisofs", "-V", label, "-o", str(output_path)]
303
+ if video:
304
+ command += ["-dvd-video", "-udf", str(source_paths[0])]
305
+ else:
306
+ command += ["-iso-level", "3", "-J", "-R", "-udf",
307
+ *[str(path) for path in source_paths]]
304
308
  else:
305
309
  raise RuntimeError("Image building is only used by non-Linux optical backends")
306
310
  subprocess.run(command, check=True, capture_output=True, text=True)
@@ -310,7 +314,8 @@ def build_data_image(source_paths, output_path, label, video=False):
310
314
  def iso_burn_command(device, image_path):
311
315
  system = system_name()
312
316
  if system == "darwin":
313
- return [tool("hdiutil"), "burn", str(image_path)]
317
+ # -puppetstrings emits machine-readable PERCENT: / MESSAGE: lines.
318
+ return [tool("hdiutil"), "burn", "-puppetstrings", str(image_path)]
314
319
  if system == "windows":
315
320
  return [tool("isoburn.exe"), "/Q", device, str(image_path)]
316
321
  raise RuntimeError("ISO command requested on Linux; use growisofs backend")