discstation 0.1.42 → 0.1.43

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/README.md CHANGED
@@ -175,7 +175,11 @@ self-signed cert. On startup the host prints the LAN URL to open
175
175
  — and picking one starts it directly, no separate confirm step. Also has
176
176
  EJECT/CLOSE toggle, a live disc-status readout, and full playback
177
177
  transport. Greys out and auto-collapses the instant a physical remote is
178
- detected, so the two never fight for control.
178
+ detected, so the two never fight for control — including mid-burn/rip/play:
179
+ plugging the remote in hands it control without interrupting whatever's
180
+ running, and its screen picks up the current status instead of starting
181
+ over; unplugging it hands control back to the on-screen remote within a
182
+ second or two.
179
183
 
180
184
  ## Mobile App
181
185
 
@@ -184,7 +188,8 @@ has the same on-screen remote as the web UI, rendered inline in the main
184
188
  screen rather than a popup — same "REMOTE" tag toggle, opens automatically
185
189
  with no ESP32 attached, mode buttons filtered to what the disc actually
186
190
  supports, and picking one starts it directly. Same auto-collapse behavior
187
- when a physical ESP32 shows up, same underlying `/remote/button` protocol.
191
+ when a physical ESP32 shows up — mid-operation and without interruption, same
192
+ as the web UI — and the same underlying `/remote/button` protocol.
188
193
 
189
194
  ## Disc Support
190
195
 
Binary file
@@ -1 +1 @@
1
- {"version":"0.1.42","size":1260592,"md5":"fc62c97a1f9e0bedc579426771789964"}
1
+ {"version":"0.1.43","size":1260592,"md5":"575faf0891e354ecd451d8454228a18e"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discstation",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs",
@@ -530,6 +530,14 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
530
530
  return
531
531
  if _active_ser is not None and _active_ser.hw is None:
532
532
  _active_ser.virtual.push_line(cmd)
533
+ # One-click burn: chain START right behind SELECT:BURN* in the SAME
534
+ # request, atomically - station_loop's SELECT handler clears any
535
+ # stale buffered input the instant it reads the SELECT line, and a
536
+ # START sent as a second, separate HTTP call can land in that exact
537
+ # gap and get wiped (intermittent, timing-dependent "burn never
538
+ # starts" bug). Queuing both lines from one call removes the gap.
539
+ if cmd.upper().startswith("SELECT:BURN"):
540
+ _active_ser.virtual.push_line("START")
533
541
  self._respond(200, 'OK')
534
542
  else:
535
543
  self._respond(409, 'A hardware remote is attached')
@@ -645,7 +653,7 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
645
653
  def _serve_sw(self):
646
654
  sw = '''self.addEventListener('install', e => {
647
655
  self.skipWaiting();
648
- caches.open('discstation-v18').then(c => c.addAll(['/','/static/style.css?v=18','/static/app.js?v=18']));
656
+ caches.open('discstation-v20').then(c => c.addAll(['/','/static/style.css?v=20','/static/app.js?v=20']));
649
657
  });
650
658
  self.addEventListener('activate', e => e.waitUntil(clients.claim()));
651
659
  self.addEventListener('fetch', e => {
@@ -654,7 +662,7 @@ self.addEventListener('fetch', e => {
654
662
  if (path === '/' || path.startsWith('/static/')) {
655
663
  e.respondWith(fetch(e.request).then(r => {
656
664
  const copy = r.clone();
657
- caches.open('discstation-v18').then(c => c.put(e.request, copy));
665
+ caches.open('discstation-v20').then(c => c.put(e.request, copy));
658
666
  return r;
659
667
  }).catch(() => caches.match(e.request)));
660
668
  } else {
@@ -845,6 +853,20 @@ def wait_for_web_url(ser):
845
853
  url = f"{protocol}://{ip}:{_web_port}"
846
854
  safe_send(ser, f"IP:{url}")
847
855
  print(f"URL displayed: {url}")
856
+ # The one-click remote buttons queue START right behind SELECT:BURN* (see
857
+ # _handle_remote_button) - when files were uploaded BEFORE the mode was
858
+ # picked (the normal one-click order), that START can already be sitting
859
+ # in the buffer by the time this runs. Check the queue before touching the
860
+ # serial line at all: the loop below reads and discards one line per pass
861
+ # looking for CANCEL/HOME, and would otherwise eat that START and throw it
862
+ # away, leaving the burn waiting forever for a START that already came.
863
+ try:
864
+ got = _burn_url_queue.get_nowait()
865
+ except Empty:
866
+ got = None
867
+ if got:
868
+ safe_send(ser, "STATUS:Got URL, starting...")
869
+ return got
848
870
  while True:
849
871
  line = read_serial_line(ser, timeout=0.5)
850
872
  if line and line.strip().upper() in ("CANCEL", "HOME"):
package/src/static/app.js CHANGED
@@ -51,11 +51,16 @@
51
51
  function setProgress(phase, percent, active = true) {
52
52
  const panel = $("global-progress");
53
53
  const fill = $("progress-fill");
54
- panel.hidden = !active && percent < 0;
54
+ // Clamp here, the one place this renders - a browser's own upload
55
+ // "progress" event isn't guaranteed to stop exactly at loaded===total
56
+ // (seen reading past 100%), and this is the single choke point every
57
+ // caller (upload, burn/rip progress, SSE) goes through either way.
58
+ const shown = percent >= 0 ? Math.min(100, Math.max(0, Math.round(percent))) : -1;
59
+ panel.hidden = !active && shown < 0;
55
60
  $("progress-phase").textContent = (phase || "READY").toUpperCase();
56
- $("progress-value").textContent = percent >= 0 ? `${percent}%` : "...";
57
- fill.style.width = percent >= 0 ? `${Math.min(100, percent)}%` : "34%";
58
- fill.classList.toggle("indeterminate", percent < 0);
61
+ $("progress-value").textContent = shown >= 0 ? `${shown}%` : "...";
62
+ fill.style.width = shown >= 0 ? `${shown}%` : "34%";
63
+ fill.classList.toggle("indeterminate", shown < 0);
59
64
  }
60
65
 
61
66
  async function pollStatus() {
@@ -154,16 +159,11 @@
154
159
  const button = event.target.closest("[data-cmd]");
155
160
  if (!button || button.disabled) return;
156
161
  const cmd = button.dataset.cmd;
162
+ // One-click burn: the server queues START right behind SELECT:BURN* in the
163
+ // same request now (was two separate POSTs from here, which raced against
164
+ // the server clearing stale input the instant it read SELECT and could
165
+ // silently drop the START - see _handle_remote_button in discstation.py).
157
166
  await sendRemoteCmd(cmd);
158
- // On real hardware START is a long-press of the encoder on the burn-ready
159
- // review screen, not its own button - a one-click "BURN AUDIO"/"BURN
160
- // DATA"/"BURN" here is the whole point of the web remote, so chase the
161
- // mode select straight through to starting the burn instead of leaving
162
- // the user stuck with nothing left to press. (The backend queues this
163
- // harmlessly if files/URL aren't in yet - it's only consumed once the
164
- // burn's actually at its own "waiting for start" step, and gets flushed
165
- // if a different mode gets selected first so it can't fire the wrong burn.)
166
- if (cmd.startsWith("SELECT:BURN")) await sendRemoteCmd("START");
167
167
  });
168
168
  let volumeTimer;
169
169
  $("remote-volume").addEventListener("input", (event) => {
@@ -144,7 +144,7 @@
144
144
  </footer>
145
145
  </div>
146
146
  <div id="install-slot"></div>
147
- <script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=18").catch(() => {});</script>
148
- <script src="/static/app.js?v=18" defer></script>
147
+ <script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=20").catch(() => {});</script>
148
+ <script src="/static/app.js?v=20" defer></script>
149
149
  </body>
150
150
  </html>