discstation 0.3.0 → 0.3.1
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/arduino/firmware/discstation.bin +0 -0
- package/arduino/firmware/firmware.json +1 -1
- package/package.json +1 -1
- package/src/discstation.py +43 -11
- package/src/static/app.js +23 -0
- package/src/static/index.html +4 -3
- package/src/static/style.css +33 -0
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.1","size":1260592,"md5":"77e56fa726b756dde0f081779164e880"}
|
package/package.json
CHANGED
package/src/discstation.py
CHANGED
|
@@ -483,6 +483,7 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
|
|
|
483
483
|
upload_dir = Path(discstation_burn.WORK) / f"upload_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
484
484
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
|
485
485
|
total = 0
|
|
486
|
+
upload_order = [] # relative paths, in the order the client sent them
|
|
486
487
|
for i, (filename, data) in enumerate(files):
|
|
487
488
|
rel = filename
|
|
488
489
|
if i < len(paths_list) and paths_list[i].get('p'):
|
|
@@ -495,7 +496,14 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
|
|
|
495
496
|
continue
|
|
496
497
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
497
498
|
dest.write_bytes(data)
|
|
499
|
+
upload_order.append(rel.lstrip('/'))
|
|
498
500
|
total += len(data)
|
|
501
|
+
# burn_audio_flow() prefers this order over its alphabetical fallback,
|
|
502
|
+
# so an audio burn tracks the order files were actually selected/
|
|
503
|
+
# arranged on the web page or app, not whatever order their filenames
|
|
504
|
+
# happen to sort in (the bug: picking "03 intro, 01 verse, 02 chorus"
|
|
505
|
+
# used to always burn 01/02/03 regardless of pick order).
|
|
506
|
+
(upload_dir / ".upload_order.json").write_text(json.dumps(upload_order))
|
|
499
507
|
# wait_for_web_url() (already running on the OLED side once a burn
|
|
500
508
|
# mode is selected first - the normal flow: scan the QR code, then
|
|
501
509
|
# upload) only ever reads from _burn_url_queue, same as a pasted
|
|
@@ -698,7 +706,7 @@ class _WebHandler(http.server.BaseHTTPRequestHandler):
|
|
|
698
706
|
def _serve_sw(self):
|
|
699
707
|
sw = '''self.addEventListener('install', e => {
|
|
700
708
|
self.skipWaiting();
|
|
701
|
-
caches.open('discstation-
|
|
709
|
+
caches.open('discstation-v48').then(c => c.addAll(['/','/static/style.css?v=48','/static/app.js?v=48']));
|
|
702
710
|
});
|
|
703
711
|
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
|
|
704
712
|
self.addEventListener('fetch', e => {
|
|
@@ -707,7 +715,7 @@ self.addEventListener('fetch', e => {
|
|
|
707
715
|
if (path === '/' || path.startsWith('/static/')) {
|
|
708
716
|
e.respondWith(fetch(e.request).then(r => {
|
|
709
717
|
const copy = r.clone();
|
|
710
|
-
caches.open('discstation-
|
|
718
|
+
caches.open('discstation-v48').then(c => c.put(e.request, copy));
|
|
711
719
|
return r;
|
|
712
720
|
}).catch(() => caches.match(e.request)));
|
|
713
721
|
} else {
|
|
@@ -1833,8 +1841,15 @@ def drive_status(device):
|
|
|
1833
1841
|
except OSError:
|
|
1834
1842
|
return "unknown"
|
|
1835
1843
|
try:
|
|
1836
|
-
|
|
1837
|
-
|
|
1844
|
+
# Hard-timeout, not just the OSError below: on a wedged/dying drive this
|
|
1845
|
+
# ioctl can block in uninterruptible D-state (blk_execute_rq) forever -
|
|
1846
|
+
# O_NONBLOCK on the fd doesn't bound a block-layer command wait. Without
|
|
1847
|
+
# this, a stuck call here holds _detect_lock forever and wedges the
|
|
1848
|
+
# whole service (every other thread needing disc state queues up behind
|
|
1849
|
+
# it) - see _run_hard_timeout's own docstring for why this is the only
|
|
1850
|
+
# way to get control back.
|
|
1851
|
+
st = _run_hard_timeout(lambda: fcntl.ioctl(fd, _CDROM_DRIVE_STATUS, 0), timeout=3)
|
|
1852
|
+
except Exception:
|
|
1838
1853
|
return "unknown"
|
|
1839
1854
|
finally:
|
|
1840
1855
|
try:
|
|
@@ -3207,13 +3222,30 @@ def burn_audio_flow(ser):
|
|
|
3207
3222
|
audio_files = []
|
|
3208
3223
|
audio_exts = {".wav", ".flac", ".mp3", ".aac", ".ogg", ".wma", ".m4a", ".opus"}
|
|
3209
3224
|
if src_path.is_dir():
|
|
3210
|
-
#
|
|
3211
|
-
#
|
|
3212
|
-
#
|
|
3213
|
-
#
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3225
|
+
# Prefer the order the web/app upload actually sent (.upload_order.json,
|
|
3226
|
+
# written by _handle_upload) over re-deriving one from the filesystem -
|
|
3227
|
+
# burning in alphabetical-by-filename order regardless of how the user
|
|
3228
|
+
# arranged/picked the files was the whole bug this guards against.
|
|
3229
|
+
order_file = src_path / ".upload_order.json"
|
|
3230
|
+
if order_file.is_file():
|
|
3231
|
+
try:
|
|
3232
|
+
for rel in json.loads(order_file.read_text()):
|
|
3233
|
+
f = src_path / rel
|
|
3234
|
+
if f.is_file() and f.suffix.lower() in audio_exts:
|
|
3235
|
+
audio_files.append(f)
|
|
3236
|
+
except Exception as e:
|
|
3237
|
+
print(f"Upload order manifest unreadable, falling back to alphabetical: {e}")
|
|
3238
|
+
audio_files = []
|
|
3239
|
+
if not audio_files:
|
|
3240
|
+
# rglob, not iterdir: a browser folder upload preserves the
|
|
3241
|
+
# original relative paths (e.g. "Album Name/track.flac"), so the
|
|
3242
|
+
# audio files usually sit one level below src_path, not at its
|
|
3243
|
+
# top - a plain iterdir() only ever saw the subfolder itself.
|
|
3244
|
+
# Also the fallback for a typed/pasted directory path (no upload,
|
|
3245
|
+
# so no order manifest) - alphabetical is the best guess there.
|
|
3246
|
+
for f in sorted(src_path.rglob("*")):
|
|
3247
|
+
if f.is_file() and f.suffix.lower() in audio_exts:
|
|
3248
|
+
audio_files.append(f)
|
|
3217
3249
|
elif src_path.is_file():
|
|
3218
3250
|
audio_files = [src_path]
|
|
3219
3251
|
|
package/src/static/app.js
CHANGED
|
@@ -327,6 +327,29 @@
|
|
|
327
327
|
});
|
|
328
328
|
initRpBars();
|
|
329
329
|
|
|
330
|
+
// Fullscreen toggle for the OLED-mirror screen (e.g. blown up on a
|
|
331
|
+
// monitor/TV while music plays). iOS Safari has no element-level
|
|
332
|
+
// Fullscreen API - feature-detect and hide the button there instead of
|
|
333
|
+
// leaving a control that does nothing.
|
|
334
|
+
{
|
|
335
|
+
const rpScreen = $("rp-screen");
|
|
336
|
+
const fsBtn = $("rp-fullscreen-btn");
|
|
337
|
+
const requestFs = rpScreen.requestFullscreen || rpScreen.webkitRequestFullscreen;
|
|
338
|
+
const exitFs = document.exitFullscreen || document.webkitExitFullscreen;
|
|
339
|
+
if (!requestFs || !exitFs) {
|
|
340
|
+
fsBtn.hidden = true;
|
|
341
|
+
} else {
|
|
342
|
+
const isFs = () => (document.fullscreenElement || document.webkitFullscreenElement) === rpScreen;
|
|
343
|
+
const syncLabel = () => fsBtn.setAttribute("aria-label", isFs() ? "Exit fullscreen" : "Fullscreen");
|
|
344
|
+
fsBtn.addEventListener("click", () => {
|
|
345
|
+
if (isFs()) exitFs.call(document);
|
|
346
|
+
else requestFs.call(rpScreen);
|
|
347
|
+
});
|
|
348
|
+
document.addEventListener("fullscreenchange", syncLabel);
|
|
349
|
+
document.addEventListener("webkitfullscreenchange", syncLabel);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
330
353
|
// Firmware row: outside the remote panel on purpose - a hardware remote hides
|
|
331
354
|
// that panel, and hardware is exactly when there is a remote to update.
|
|
332
355
|
function applyFirmwareState(progress) {
|
package/src/static/index.html
CHANGED
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
<button class="rp-btn rp-trackbtn" type="button" data-role="prev" data-cmd="REW:BIG" aria-label="Previous track"><span class="rp-glyph">⏮</span></button>
|
|
64
64
|
<button class="rp-btn rp-trackbtn" type="button" data-role="next" data-cmd="FF:BIG" aria-label="Next track"><span class="rp-glyph">⏭</span></button>
|
|
65
65
|
</div>
|
|
66
|
-
<div class="rp-screen">
|
|
66
|
+
<div id="rp-screen" class="rp-screen">
|
|
67
|
+
<button id="rp-fullscreen-btn" class="rp-fullscreen-btn" type="button" aria-label="Fullscreen">⛶</button>
|
|
67
68
|
<div id="rp-status" class="rp-status">READY</div>
|
|
68
69
|
<div id="rp-bars" class="rp-bars"></div>
|
|
69
70
|
<div id="rp-discsaver" class="rp-discsaver" hidden></div>
|
|
@@ -151,7 +152,7 @@
|
|
|
151
152
|
</footer>
|
|
152
153
|
</div>
|
|
153
154
|
<div id="install-slot"></div>
|
|
154
|
-
<script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=
|
|
155
|
-
<script src="/static/app.js?v=
|
|
155
|
+
<script>if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js?v=48").catch(() => {});</script>
|
|
156
|
+
<script src="/static/app.js?v=48" defer></script>
|
|
156
157
|
</body>
|
|
157
158
|
</html>
|
package/src/static/style.css
CHANGED
|
@@ -317,6 +317,7 @@ h2 { margin-top: 8px; font-family: var(--display); font-size: clamp(30px, 7vw, 6
|
|
|
317
317
|
}
|
|
318
318
|
.rp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
319
319
|
.rp-screen {
|
|
320
|
+
position: relative;
|
|
320
321
|
background: #0a0a0a;
|
|
321
322
|
border: 1px solid var(--ink);
|
|
322
323
|
border-radius: 3px;
|
|
@@ -328,6 +329,38 @@ h2 { margin-top: 8px; font-family: var(--display); font-size: clamp(30px, 7vw, 6
|
|
|
328
329
|
flex-direction: column;
|
|
329
330
|
gap: 8px;
|
|
330
331
|
}
|
|
332
|
+
/* Corner overlay, not a layout element - doesn't push the status/bars around. */
|
|
333
|
+
.rp-fullscreen-btn {
|
|
334
|
+
position: absolute;
|
|
335
|
+
top: 4px;
|
|
336
|
+
right: 6px;
|
|
337
|
+
z-index: 1;
|
|
338
|
+
border: 0;
|
|
339
|
+
background: none;
|
|
340
|
+
color: #8a8a8a;
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
line-height: 1;
|
|
343
|
+
padding: 2px;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
opacity: 0.5;
|
|
346
|
+
transition: opacity 0.15s linear;
|
|
347
|
+
}
|
|
348
|
+
.rp-fullscreen-btn:hover, .rp-fullscreen-btn:focus-visible { opacity: 1; }
|
|
349
|
+
.rp-fullscreen-btn[hidden] { display: none; }
|
|
350
|
+
/* Blown up to fill the display (e.g. a monitor/TV while music plays) - same
|
|
351
|
+
OLED look, just bigger. .rp-bars/.rp-bar-col need no changes: they already
|
|
352
|
+
fill available space, and buildRpBars' ResizeObserver (app.js) recomputes
|
|
353
|
+
bar count from the now much wider #rp-bars on its own. */
|
|
354
|
+
.rp-screen:fullscreen, .rp-screen:-webkit-full-screen {
|
|
355
|
+
padding: 28px 32px 24px;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
}
|
|
358
|
+
.rp-screen:fullscreen .rp-status, .rp-screen:-webkit-full-screen .rp-status {
|
|
359
|
+
font-size: 22px;
|
|
360
|
+
}
|
|
361
|
+
.rp-screen:fullscreen .rp-fullscreen-btn, .rp-screen:-webkit-full-screen .rp-fullscreen-btn {
|
|
362
|
+
font-size: 20px;
|
|
363
|
+
}
|
|
331
364
|
.rp-status {
|
|
332
365
|
color: #f2f2f2;
|
|
333
366
|
font-family: var(--mono);
|