discstation 0.1.17 → 0.1.18

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.17",
3
+ "version": "0.1.18",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs",
@@ -53,6 +53,8 @@ _last_upload_label = None
53
53
  _web_status = "READY"
54
54
  _web_progress = -1
55
55
  _web_progress_active = False
56
+ _operation_active = False # a burn/rip/play flow is holding the drive
57
+ _last_disc_info = {"disc_present": False, "capacity_bytes": 0, "capacity_gb": 0, "type": "none"}
56
58
  _active_ser = None
57
59
  STATIC_DIR = Path(__file__).resolve().parent / "static"
58
60
 
@@ -146,6 +148,10 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
146
148
  self._respond(200, f'{len(files)} file(s) uploaded ({size_str}). Select BURN DATA on remote.')
147
149
 
148
150
  def _serve_disc_info(self):
151
+ if _operation_active:
152
+ # a burn/rip/play holds the drive — don't probe it, serve last-known.
153
+ self._respond(200, json.dumps({**_last_disc_info, "busy": True}), "application/json")
154
+ return
149
155
  info = {"disc_present": False, "capacity_bytes": 0, "capacity_gb": 0, "type": "none"}
150
156
  try:
151
157
  device = discstation_burn.disc_device()
@@ -163,6 +169,7 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
163
169
  info["label"] = di.label
164
170
  except Exception as e:
165
171
  print(f"Disc info error: {e}")
172
+ _last_disc_info.update(info)
166
173
  self._respond(200, json.dumps(info), "application/json")
167
174
 
168
175
  def _serve_sse(self):
@@ -212,7 +219,7 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
212
219
  def _serve_sw(self):
213
220
  sw = '''self.addEventListener('install', e => {
214
221
  self.skipWaiting();
215
- caches.open('discstation-v7').then(c => c.addAll(['/','/static/style.css?v=7','/static/app.js?v=7']));
222
+ caches.open('discstation-v8').then(c => c.addAll(['/','/static/style.css?v=8','/static/app.js?v=8']));
216
223
  });
217
224
  self.addEventListener('activate', e => e.waitUntil(clients.claim()));
218
225
  self.addEventListener('fetch', e => {
@@ -221,7 +228,7 @@ self.addEventListener('fetch', e => {
221
228
  if (path === '/' || path.startsWith('/static/')) {
222
229
  e.respondWith(fetch(e.request).then(r => {
223
230
  const copy = r.clone();
224
- caches.open('discstation-v7').then(c => c.put(e.request, copy));
231
+ caches.open('discstation-v8').then(c => c.put(e.request, copy));
225
232
  return r;
226
233
  }).catch(() => caches.match(e.request)));
227
234
  } else {
@@ -3731,7 +3738,7 @@ def rip_audio_cd(ser, device, artist_hint=None, album_hint=None):
3731
3738
 
3732
3739
 
3733
3740
  def station_loop(ser, url, artist_hint=None, album_hint=None):
3734
- global _last_burn_result, _last_burn_result_time, _tray_open, _tray_open_since
3741
+ global _last_burn_result, _last_burn_result_time, _tray_open, _tray_open_since, _operation_active
3735
3742
  discstation_burn.cleanup_old_jobs()
3736
3743
  try:
3737
3744
  device = discstation_burn.disc_device()
@@ -3934,6 +3941,7 @@ def station_loop(ser, url, artist_hint=None, album_hint=None):
3934
3941
  # The user picked a mode — they want to act on a disc, so the drive is
3935
3942
  # fair game again even if it was ejected from the OLED earlier.
3936
3943
  _tray_open = False
3944
+ _operation_active = True # stop /disc-info probing the drive during the flow
3937
3945
  try:
3938
3946
  if mode == "BURN":
3939
3947
  burn_flow(ser, url)
@@ -3964,6 +3972,8 @@ def station_loop(ser, url, artist_hint=None, album_hint=None):
3964
3972
  _last_burn_result = f"ERROR: {e}"
3965
3973
  print(f"Error in {mode}: {e}")
3966
3974
  time.sleep(4)
3975
+ finally:
3976
+ _operation_active = False
3967
3977
 
3968
3978
  _last_burn_result_time = time.time()
3969
3979
  refresh_main_menu(ser)
@@ -1274,22 +1274,31 @@ def _run_growisofs(ser, growisofs_cmd, log_path, device=None):
1274
1274
 
1275
1275
  def _run_hdiutil_burn(ser, image_path, device=None):
1276
1276
  """Burn a pre-built ISO on macOS via `hdiutil burn -puppetstrings`, streaming
1277
- its PERCENT: lines to the ESP32."""
1277
+ its PERCENT: lines to the ESP32 for both the write and verify passes."""
1278
1278
  send(ser, "STATUS:Burning image...")
1279
1279
  send(ser, "PROGRESS:0%")
1280
1280
  cmd = discstation_host.iso_burn_command(device or disc_device(), image_path)
1281
+ if discstation_host.system_name() == "darwin":
1282
+ # hdiutil block-buffers stdout to a pipe -> no progress until it exits.
1283
+ # Run it under a pty (script relays the child's exit status verbatim).
1284
+ cmd = ["/usr/bin/script", "-q", "/dev/null", *cmd]
1281
1285
  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
1282
1286
  out_lines = []
1283
1287
  last_pct = -1
1288
+ phase = "burn"
1284
1289
  try:
1285
1290
  for line in iter_proc_or_cancel(proc, ser):
1286
1291
  out_lines.append(line)
1287
- m = re.search(r"PERCENT:([\d.]+)", line)
1288
- if m:
1289
- pct = int(float(m.group(1)))
1290
- if 0 <= pct <= 100 and pct != last_pct:
1291
- last_pct = pct
1292
- send(ser, f"PROGRESS:{min(pct, 99)}%")
1292
+ low = line.lower()
1293
+ m = re.search(r"PERCENT:(-?[\d.]+)", line)
1294
+ pct = int(float(m.group(1))) if m else None
1295
+ if phase == "burn" and ("verif" in low or (pct is not None and last_pct > 90 and pct < 5)):
1296
+ phase = "verify"
1297
+ last_pct = -1
1298
+ send(ser, "STATUS:Verifying...")
1299
+ if pct is not None and 0 <= pct <= 100 and pct != last_pct:
1300
+ last_pct = pct
1301
+ send(ser, f"PROGRESS:{min(pct, 99)}%")
1293
1302
  except (KeyboardInterrupt, SystemExit):
1294
1303
  stop_process(proc)
1295
1304
  raise
@@ -1300,8 +1309,11 @@ def _run_hdiutil_burn(ser, image_path, device=None):
1300
1309
  safe_send(ser, "PROGRESS:100%")
1301
1310
  try:
1302
1311
  discstation_host.eject_device(device or disc_device())
1303
- except Exception as e:
1304
- print(f"Disc eject skipped: {e}")
1312
+ except Exception:
1313
+ try:
1314
+ discstation_host.eject_device(None) # device-less `drutil eject`
1315
+ except Exception as e:
1316
+ print(f"Disc eject skipped: {e}")
1305
1317
 
1306
1318
 
1307
1319
  def burn(ser, dvd_dir, disc_label, speed=None, is_dual_layer=False):
package/src/static/app.js CHANGED
@@ -77,6 +77,7 @@
77
77
  try {
78
78
  const response = await fetch("/disc-info", { cache: "no-store" });
79
79
  const info = await response.json();
80
+ if (info.busy) return; // burn/rip in progress — keep current
80
81
  state.discBytes = Number(info.capacity_bytes || 0);
81
82
  state.discType = info.type || "none";
82
83
  renderSelection();
@@ -281,7 +282,7 @@
281
282
  setInterval(loadDiscInfo, 15000);
282
283
  }
283
284
 
284
- if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=7").catch(() => {});
285
+ if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=8").catch(() => {});
285
286
  window.addEventListener("beforeinstallprompt", (event) => {
286
287
  event.preventDefault();
287
288
  const button = document.createElement("button");
@@ -105,7 +105,7 @@
105
105
  </footer>
106
106
  </div>
107
107
  <div id="install-slot"></div>
108
- <script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=7").catch(() => {});</script>
109
- <script src="/static/app.js?v=7" defer></script>
108
+ <script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=8").catch(() => {});</script>
109
+ <script src="/static/app.js?v=8" defer></script>
110
110
  </body>
111
111
  </html>