discstation 0.1.1 → 0.1.6

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 CHANGED
@@ -64,10 +64,12 @@ cat > "$PLIST" <<PLIST
64
64
  <key>Label</key><string>com.discstation.agent</string>
65
65
  <key>ProgramArguments</key><array>
66
66
  <string>$VENV_DIR/bin/python</string>
67
+ <string>-u</string>
67
68
  <string>$APP_DIR/discstation.py</string>
68
69
  <string>--port</string><string>8080</string>
69
70
  </array>
70
71
  <key>EnvironmentVariables</key><dict>
72
+ <key>PYTHONUNBUFFERED</key><string>1</string>
71
73
  <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
72
74
  <key>DISC_DEVICE</key><string>${DISC_DEVICE:-}</string>
73
75
  <key>DISC_PORT</key><string>${DISC_PORT:-}</string>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discstation",
3
- "version": "0.1.1",
3
+ "version": "0.1.6",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs"
@@ -918,6 +918,8 @@ _tray_open_since = 0.0 # time.monotonic() of the last OLED-initiated eject
918
918
 
919
919
 
920
920
  def _tray_closed_with_disc(device):
921
+ if not device:
922
+ return False
921
923
  global _tray_open
922
924
  if not Path(device).exists():
923
925
  return False
@@ -932,6 +934,8 @@ def _tray_closed_with_disc(device):
932
934
 
933
935
 
934
936
  def disc_present(device):
937
+ if not device:
938
+ return False
935
939
  if _tray_closed_with_disc(device):
936
940
  return True
937
941
  if _tray_open:
@@ -968,6 +972,8 @@ def disc_present(device):
968
972
 
969
973
 
970
974
  def is_blank_disc(device):
975
+ if not device:
976
+ return False
971
977
  if not Path(device).exists():
972
978
  return False
973
979
 
@@ -1017,6 +1023,8 @@ def is_blank_disc(device):
1017
1023
 
1018
1024
 
1019
1025
  def is_rewritable_disc(device):
1026
+ if not device:
1027
+ return False
1020
1028
  """Return whether the inserted medium can be overwritten."""
1021
1029
  if not Path(device).exists():
1022
1030
  return False
@@ -1100,6 +1108,8 @@ def drive_status(device):
1100
1108
  Returns 'disc' | 'no_disc' | 'open' | 'loading' | 'unknown'. Single ioctl on
1101
1109
  an O_NONBLOCK fd — does not consult the (stale on this USB bridge) udev db and
1102
1110
  does not disturb the tray. Never raises."""
1111
+ if not device:
1112
+ return "unknown"
1103
1113
  try:
1104
1114
  fd = os.open(device, os.O_RDONLY | os.O_NONBLOCK)
1105
1115
  except OSError:
@@ -1118,6 +1128,8 @@ def drive_status(device):
1118
1128
 
1119
1129
  def _media_quick_state(device, props):
1120
1130
  """Fast, cheap read of whether a disc is loaded. No long probes."""
1131
+ if not device:
1132
+ return "empty"
1121
1133
  if _tray_open:
1122
1134
  return "empty" # deliberate OLED eject — do not poke the drive
1123
1135
  st = drive_status(device)
@@ -1402,6 +1414,8 @@ def detect_disc(device, settle=True, budget=None, force=False):
1402
1414
  disc_status_line / disc_title / menu_items_for_disc in one refresh burst do a
1403
1415
  single probe. `settle=False` skips the post-insert wait (used by the web
1404
1416
  endpoint, which can just poll again)."""
1417
+ if not device:
1418
+ return _disc_info(False, "none")
1405
1419
  if _tray_open:
1406
1420
  # Tray was deliberately ejected from the OLED — report "no disc" without
1407
1421
  # touching the drive (cdrom_id / wodim / open() would re-close the tray).
@@ -3654,7 +3668,10 @@ def rip_audio_cd(ser, device, artist_hint=None, album_hint=None):
3654
3668
  def station_loop(ser, url, artist_hint=None, album_hint=None):
3655
3669
  global _last_burn_result, _last_burn_result_time, _tray_open, _tray_open_since
3656
3670
  discstation_burn.cleanup_old_jobs()
3657
- device = discstation_burn.disc_device()
3671
+ try:
3672
+ device = discstation_burn.disc_device()
3673
+ except Exception:
3674
+ device = None # empty drive (e.g. macOS after an eject) — keep the loop alive
3658
3675
 
3659
3676
  for _ in range(50):
3660
3677
  line = read_serial_line(ser, timeout=0.2)
@@ -3708,7 +3725,10 @@ def station_loop(ser, url, artist_hint=None, album_hint=None):
3708
3725
  _disc_poll_start = 0
3709
3726
 
3710
3727
  def _poll_disc():
3711
- current_device = discstation_burn.disc_device()
3728
+ try:
3729
+ current_device = discstation_burn.disc_device()
3730
+ except Exception:
3731
+ return "Disc: none", None # no drive/media visible right now
3712
3732
  return disc_status_line(current_device), current_device
3713
3733
 
3714
3734
  def apply_disc_line(new_line):
@@ -96,15 +96,25 @@ def _mac_optical_device():
96
96
  return f"/dev/{match.group(1)}" if match else None
97
97
 
98
98
 
99
+ _last_disc_device = None
100
+
101
+
99
102
  def disc_device():
103
+ global _last_disc_device
100
104
  system = system_name()
101
105
  override = os.environ.get("DISC_DEVICE") or os.environ.get("DVD_DEVICE")
102
106
  if system == "darwin":
103
107
  detected = _mac_optical_device()
104
108
  if detected:
109
+ _last_disc_device = detected
105
110
  return detected
106
- if override and Path(override).exists():
111
+ if override:
107
112
  return override
113
+ # macOS: the /dev/diskN node only exists while media is loaded. Once a
114
+ # disc is ejected there is nothing to detect — keep returning the last
115
+ # known node so the caller can still probe it (it just reports no media).
116
+ if _last_disc_device:
117
+ return _last_disc_device
108
118
  elif override:
109
119
  return override
110
120
 
@@ -364,10 +374,14 @@ def eject_device(device, close=False):
364
374
  command.append("-t")
365
375
  command.append(device)
366
376
  elif system == "darwin":
367
- action = "close" if close else "open"
368
- commands = [["/usr/bin/drutil", "tray", action]]
369
- if device:
370
- commands.append(["/usr/sbin/diskutil", "eject", device])
377
+ # `drutil tray open` is a no-op on slot-load drives; `drutil eject`
378
+ # works on both. `diskutil eject` is the fallback for a mounted disc.
379
+ if close:
380
+ commands = [["/usr/bin/drutil", "tray", "close"]]
381
+ else:
382
+ commands = [["/usr/bin/drutil", "eject"]]
383
+ if device:
384
+ commands.append(["/usr/sbin/diskutil", "eject", device])
371
385
  for command in commands:
372
386
  try:
373
387
  if subprocess.run(command, capture_output=True, text=True, timeout=10).returncode == 0:
@@ -4,7 +4,7 @@ After=network.target
4
4
 
5
5
  [Service]
6
6
  EnvironmentFile=-%h/.local/share/discstation/discstation.env
7
- ExecStart=%h/.local/share/discstation/venv/bin/python %h/.local/share/discstation/app/discstation.py
7
+ ExecStart=%h/.local/share/discstation/venv/bin/python -u %h/.local/share/discstation/app/discstation.py
8
8
  Restart=on-failure
9
9
  RestartSec=3
10
10
  KillSignal=SIGKILL