discstation 0.1.2 → 0.1.7

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.2",
3
+ "version": "0.1.7",
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,10 @@ 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"
1113
+ if discstation_host.system_name() != "linux":
1114
+ return discstation_host.drive_status()
1103
1115
  try:
1104
1116
  fd = os.open(device, os.O_RDONLY | os.O_NONBLOCK)
1105
1117
  except OSError:
@@ -1118,6 +1130,8 @@ def drive_status(device):
1118
1130
 
1119
1131
  def _media_quick_state(device, props):
1120
1132
  """Fast, cheap read of whether a disc is loaded. No long probes."""
1133
+ if not device:
1134
+ return "empty"
1121
1135
  if _tray_open:
1122
1136
  return "empty" # deliberate OLED eject — do not poke the drive
1123
1137
  st = drive_status(device)
@@ -1402,6 +1416,8 @@ def detect_disc(device, settle=True, budget=None, force=False):
1402
1416
  disc_status_line / disc_title / menu_items_for_disc in one refresh burst do a
1403
1417
  single probe. `settle=False` skips the post-insert wait (used by the web
1404
1418
  endpoint, which can just poll again)."""
1419
+ if not device:
1420
+ return _disc_info(False, "none")
1405
1421
  if _tray_open:
1406
1422
  # Tray was deliberately ejected from the OLED — report "no disc" without
1407
1423
  # touching the drive (cdrom_id / wodim / open() would re-close the tray).
@@ -3654,7 +3670,10 @@ def rip_audio_cd(ser, device, artist_hint=None, album_hint=None):
3654
3670
  def station_loop(ser, url, artist_hint=None, album_hint=None):
3655
3671
  global _last_burn_result, _last_burn_result_time, _tray_open, _tray_open_since
3656
3672
  discstation_burn.cleanup_old_jobs()
3657
- device = discstation_burn.disc_device()
3673
+ try:
3674
+ device = discstation_burn.disc_device()
3675
+ except Exception:
3676
+ device = None # empty drive (e.g. macOS after an eject) — keep the loop alive
3658
3677
 
3659
3678
  for _ in range(50):
3660
3679
  line = read_serial_line(ser, timeout=0.2)
@@ -3708,7 +3727,10 @@ def station_loop(ser, url, artist_hint=None, album_hint=None):
3708
3727
  _disc_poll_start = 0
3709
3728
 
3710
3729
  def _poll_disc():
3711
- current_device = discstation_burn.disc_device()
3730
+ try:
3731
+ current_device = discstation_burn.disc_device()
3732
+ except Exception:
3733
+ return "Disc: none", None # no drive/media visible right now
3712
3734
  return disc_status_line(current_device), current_device
3713
3735
 
3714
3736
  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
 
@@ -134,6 +144,25 @@ def disc_device():
134
144
  raise FileNotFoundError("No optical disc drive found; set DISC_DEVICE explicitly")
135
145
 
136
146
 
147
+ def drive_status():
148
+ """Non-Linux equivalent of the CDROM_DRIVE_STATUS ioctl.
149
+ Returns 'disc' | 'no_disc' | 'unknown'. macOS: parse `drutil status`."""
150
+ if system_name() != "darwin":
151
+ return "unknown"
152
+ try:
153
+ r = subprocess.run(["/usr/bin/drutil", "status"], capture_output=True,
154
+ text=True, check=False, timeout=4)
155
+ except (OSError, subprocess.TimeoutExpired):
156
+ return "unknown"
157
+ text = (r.stdout + r.stderr).lower()
158
+ if "no media" in text or "media is not present" in text:
159
+ return "no_disc"
160
+ if "name: /dev/disk" in text or "type: cd" in text or "type: dvd" in text \
161
+ or "type: bd" in text:
162
+ return "disc"
163
+ return "unknown"
164
+
165
+
137
166
  def _drutil_media_properties():
138
167
  result = subprocess.run(["/usr/bin/drutil", "status"], capture_output=True, text=True, check=False, timeout=3)
139
168
  text = result.stdout + result.stderr
@@ -364,10 +393,14 @@ def eject_device(device, close=False):
364
393
  command.append("-t")
365
394
  command.append(device)
366
395
  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])
396
+ # `drutil tray open` is a no-op on slot-load drives; `drutil eject`
397
+ # works on both. `diskutil eject` is the fallback for a mounted disc.
398
+ if close:
399
+ commands = [["/usr/bin/drutil", "tray", "close"]]
400
+ else:
401
+ commands = [["/usr/bin/drutil", "eject"]]
402
+ if device:
403
+ commands.append(["/usr/sbin/diskutil", "eject", device])
371
404
  for command in commands:
372
405
  try:
373
406
  if subprocess.run(command, capture_output=True, text=True, timeout=10).returncode == 0: