discstation 0.1.35 → 0.1.38
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 +10 -7
- package/arduino/DiscStation/DiscStation.ino +34 -30
- package/discstation.env.example +1 -1
- package/docs/PLATFORM_SUPPORT.md +13 -31
- package/install-macos.sh +3 -6
- package/install.sh +6 -1
- package/package.json +1 -1
- package/requirements-optional.txt +5 -7
- package/scripts/check.sh +61 -0
- package/src/discstation.py +179 -729
- package/src/discstation_burn.py +18 -207
- package/src/static/style.css +0 -7
- package/src/win/_imapi.ps1 +66 -0
- package/src/win/audio-toc.ps1 +1 -9
- package/src/win/burn-audio.ps1 +2 -14
- package/src/win/burn-data.ps1 +4 -47
- package/src/win/burn-image.ps1 +4 -53
package/README.md
CHANGED
|
@@ -46,8 +46,7 @@ remote can talk to the host over Wi-Fi instead of a USB cable.
|
|
|
46
46
|
|------|-------------|
|
|
47
47
|
| **Burn Video DVD** | YouTube URL or local file → ffmpeg 2-pass → DVD-Video disc |
|
|
48
48
|
| **Burn Data DVD** | Any files/folders → ISO/Joliet data disc (no quality loss) |
|
|
49
|
-
| **
|
|
50
|
-
| **Play** | Playback via mpv (DVD-Video, Audio CD, VCD, SVCD) — the OLED remote shows a live spectrum visualizer of the actual audio (Linux only for now; see `docs/PLATFORM_SUPPORT.md`) |
|
|
49
|
+
| **Play** | Playback via mpv (DVD-Video, Audio CD, VCD, SVCD) — the OLED remote shows a live spectrum visualizer of the actual audio (Linux only for now; see `docs/PLATFORM_SUPPORT.md`); while a track plays the OLED keeps the track/volume screen (or the visualizer) up — the spinning-disc screensaver only appears on the home/standby screens or after playback has been paused for 15 s |
|
|
51
50
|
| **Rip** | Audio CD → FLAC (MusicBrainz); DVD-Video → VIDEO_TS mirror or HandBrake MKV (TMDb naming) |
|
|
52
51
|
|
|
53
52
|
## Project Structure
|
|
@@ -63,6 +62,7 @@ remote can talk to the host over Wi-Fi instead of a USB cable.
|
|
|
63
62
|
├── mobile/ # Expo (React Native) companion app
|
|
64
63
|
├── arduino/DiscStation/ # ESP32 DevKit remote firmware
|
|
65
64
|
├── scripts/setup.mjs # `discstation-setup` — cross-OS installer dispatch
|
|
65
|
+
├── scripts/check.sh # static checks + smoke test (run before committing)
|
|
66
66
|
├── docs/ # Platform support notes
|
|
67
67
|
├── install.sh / install-macos.sh / install-windows.ps1
|
|
68
68
|
├── requirements.txt / requirements-optional.txt
|
|
@@ -94,7 +94,9 @@ dock/taskbar icon on macOS, Linux, and Windows.
|
|
|
94
94
|
**Updating:** `npm install -g discstation` alone now redeploys and restarts
|
|
95
95
|
the already-running service automatically (a `postinstall` hook copies the
|
|
96
96
|
updated files over and restarts it) — no need to re-run `discstation-setup`
|
|
97
|
-
after every update.
|
|
97
|
+
after every update. Note this deploys whatever version is *published*, so installing an
|
|
98
|
+
older published release over a newer git checkout rolls the running service
|
|
99
|
+
back to it until you publish (or re-copy `src/`) again. `discstation-setup` itself is still what performs the
|
|
98
100
|
first install (system packages, venv, cert, service registration).
|
|
99
101
|
|
|
100
102
|
Support by OS:
|
|
@@ -154,10 +156,11 @@ self-signed cert. On startup the host prints the LAN URL to open
|
|
|
154
156
|
## Mobile App
|
|
155
157
|
|
|
156
158
|
The `mobile/` Expo app (enter the host's LAN address in its settings screen)
|
|
157
|
-
has the same on-screen remote as the web UI
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
has the same on-screen remote as the web UI, rendered inline in the main
|
|
160
|
+
screen rather than a popup — same "REMOTE" tag toggle, opens automatically
|
|
161
|
+
with no ESP32 attached, mode buttons filtered to what the disc actually
|
|
162
|
+
supports, and picking one starts it directly. Same auto-collapse behavior
|
|
163
|
+
when a physical ESP32 shows up, same underlying `/remote/button` protocol.
|
|
161
164
|
|
|
162
165
|
## Disc Support
|
|
163
166
|
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
#define LONG_PRESS_MS 1000
|
|
34
34
|
#define ENC_CLICK_GUARD_MS 200 // ignore new SW presses this soon after a rotation tick (rotation vibration can bounce SW low)
|
|
35
35
|
#define DONE_RESET_MS 30000
|
|
36
|
-
#define STANDBY_BLANK_MS 60000
|
|
37
36
|
#define IDLE_BLANK_MS 15000 // no input this long on HOME/STANDBY -> spinning-disc screensaver
|
|
38
37
|
// (this power bank's no-load auto-shutoff trips at ~30s idle; keep this
|
|
39
38
|
// well under that so the screensaver's current draw beats it there)
|
|
@@ -222,6 +221,8 @@ String discLine = "Disc: none";
|
|
|
222
221
|
String line1 = "Status: READY";
|
|
223
222
|
String line2 = "";
|
|
224
223
|
String line3 = "";
|
|
224
|
+
bool trayOpen = false; // host says the tray is open (eject wait / STANDBY:Tray open) - EJECT button then closes it
|
|
225
|
+
bool waitingIsTray = false; // the WAITING screen is the eject "close tray" wait, not the pre-burn confirm
|
|
225
226
|
String waitingLine1 = "";
|
|
226
227
|
String waitingLine2 = "";
|
|
227
228
|
int progressPercent = -1;
|
|
@@ -272,6 +273,7 @@ uint8_t vuLevel[VU_BARS];
|
|
|
272
273
|
bool visualizerActive = false;
|
|
273
274
|
unsigned long lastVuAt = 0;
|
|
274
275
|
unsigned long vuSuppressUntil = 0; // bars withheld (text screen shown instead) until millis() reaches this
|
|
276
|
+
bool playPaused = false; // last PLAY_STATUS was PAUSED - the only PLAY state the idle screensaver may take over
|
|
275
277
|
bool vuSeenReal = false; // has this PLAY session ever gotten a VU: frame with a nonzero bar -
|
|
276
278
|
// a session-level fact (macOS: never; Linux: within the first frame or
|
|
277
279
|
// two), decided once and left alone. Deliberately separate from
|
|
@@ -456,7 +458,7 @@ void drawWaiting() {
|
|
|
456
458
|
printUpper(waitingLine2);
|
|
457
459
|
|
|
458
460
|
display.setCursor(2, 49);
|
|
459
|
-
display.print("SHORT // GO");
|
|
461
|
+
display.print(waitingIsTray ? "EJECT BTN // CLOSE" : "SHORT // GO");
|
|
460
462
|
|
|
461
463
|
display.display();
|
|
462
464
|
}
|
|
@@ -774,6 +776,7 @@ void parseMessage(String msg) {
|
|
|
774
776
|
Out.println(msg);
|
|
775
777
|
|
|
776
778
|
if (msg.startsWith("HOME:")) {
|
|
779
|
+
trayOpen = false;
|
|
777
780
|
drawHome();
|
|
778
781
|
|
|
779
782
|
} else if (msg.startsWith("DISC:")) {
|
|
@@ -802,6 +805,7 @@ void parseMessage(String msg) {
|
|
|
802
805
|
if (uiState == UI_HOME) drawHome();
|
|
803
806
|
|
|
804
807
|
} else if (msg.startsWith("STANDBY:")) {
|
|
808
|
+
trayOpen = msg.substring(8) == "Tray open";
|
|
805
809
|
drawStandby();
|
|
806
810
|
|
|
807
811
|
} else if (msg.startsWith("TITLE:")) {
|
|
@@ -827,6 +831,9 @@ void parseMessage(String msg) {
|
|
|
827
831
|
lastVuAt = 0; // fresh session - unknown yet whether the host even sends VU: at all
|
|
828
832
|
visualizerActive = false;
|
|
829
833
|
vuSeenReal = false; // unknown yet whether this session ever gets a real (nonzero) frame
|
|
834
|
+
playPaused = false;
|
|
835
|
+
displayBlank = false; // a fresh session must never inherit a running screensaver
|
|
836
|
+
lastInputTime = millis();
|
|
830
837
|
Out.print("POT:"); // push the last-used volume so playback starts at it
|
|
831
838
|
Out.println(playVolume);
|
|
832
839
|
drawPlay();
|
|
@@ -838,6 +845,9 @@ void parseMessage(String msg) {
|
|
|
838
845
|
} else if (msg.startsWith("PLAY_STATUS:")) {
|
|
839
846
|
line1 = msg.substring(12);
|
|
840
847
|
if (line1.length() > 20) line1 = line1.substring(0, 20);
|
|
848
|
+
playPaused = (line1 == "PAUSED");
|
|
849
|
+
if (playPaused) lastInputTime = millis(); // idle timer starts from the pause
|
|
850
|
+
else displayBlank = false; // resumed (even from the web remote) - drop the screensaver
|
|
841
851
|
if (line1 == "PLAYING" || line1 == "PAUSED" || (audioPlayMode && line1.startsWith("TRACK "))) {
|
|
842
852
|
playStatusTemp = false;
|
|
843
853
|
} else {
|
|
@@ -849,8 +859,13 @@ void parseMessage(String msg) {
|
|
|
849
859
|
} else if (msg.startsWith("WAITING:")) {
|
|
850
860
|
returnToHomeAt = 0;
|
|
851
861
|
String rest = msg.substring(8);
|
|
862
|
+
waitingIsTray = rest.indexOf("close tray") >= 0;
|
|
863
|
+
if (waitingIsTray) trayOpen = true;
|
|
852
864
|
int slash = rest.indexOf('/');
|
|
853
|
-
if (
|
|
865
|
+
if (waitingIsTray) {
|
|
866
|
+
waitingLine1 = "TRAY OPEN";
|
|
867
|
+
waitingLine2 = "";
|
|
868
|
+
} else if (slash >= 0) {
|
|
854
869
|
waitingLine1 = "Data: " + rest.substring(0, slash);
|
|
855
870
|
waitingLine1.trim();
|
|
856
871
|
waitingLine2 = "Disc: " + rest.substring(slash + 1);
|
|
@@ -983,14 +998,6 @@ void handleSelectPress(bool longPress) {
|
|
|
983
998
|
drawStatus();
|
|
984
999
|
}
|
|
985
1000
|
|
|
986
|
-
} else if (uiState == UI_STANDBY) {
|
|
987
|
-
Out.println("EJECT");
|
|
988
|
-
line1 = "Ejecting...";
|
|
989
|
-
line2 = "";
|
|
990
|
-
line3 = "";
|
|
991
|
-
returnToHomeAt = millis() + 5000;
|
|
992
|
-
drawStatus();
|
|
993
|
-
|
|
994
1001
|
} else if (uiState == UI_STATUS && line1 == "Ejecting...") {
|
|
995
1002
|
// Manual escape if stuck on eject screen with no app response
|
|
996
1003
|
drawStandby();
|
|
@@ -1005,7 +1012,7 @@ void handleSelectPress(bool longPress) {
|
|
|
1005
1012
|
if (longPress) {
|
|
1006
1013
|
Out.println("CANCEL");
|
|
1007
1014
|
drawHome();
|
|
1008
|
-
} else {
|
|
1015
|
+
} else if (!waitingIsTray) { // tray close moved to the EJECT button
|
|
1009
1016
|
Out.println("CONFIRM");
|
|
1010
1017
|
line1 = "Confirmed!";
|
|
1011
1018
|
line2 = "";
|
|
@@ -1138,7 +1145,15 @@ void handleEncoderCCW() {
|
|
|
1138
1145
|
void handleEjectButton() {
|
|
1139
1146
|
if (wakeDisplay()) { lastInputTime = millis(); return; }
|
|
1140
1147
|
lastInputTime = millis();
|
|
1141
|
-
if (
|
|
1148
|
+
if (trayOpen && (uiState == UI_STANDBY || uiState == UI_HOME || (uiState == UI_WAITING && waitingIsTray))) {
|
|
1149
|
+
Out.println("CONFIRM"); // host closes the tray
|
|
1150
|
+
trayOpen = false;
|
|
1151
|
+
line1 = "Closing tray...";
|
|
1152
|
+
line2 = "";
|
|
1153
|
+
line3 = "";
|
|
1154
|
+
returnToHomeAt = millis() + 5000;
|
|
1155
|
+
drawStatus();
|
|
1156
|
+
} else if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_DISCONNECTED || uiState == UI_PLAY) {
|
|
1142
1157
|
Out.println("EJECT");
|
|
1143
1158
|
line1 = "Ejecting...";
|
|
1144
1159
|
line2 = "";
|
|
@@ -1353,24 +1368,13 @@ void loop() {
|
|
|
1353
1368
|
if (uiState == UI_PLAY && !displayBlank) drawPlay();
|
|
1354
1369
|
}
|
|
1355
1370
|
|
|
1356
|
-
// --- Idle disc screensaver (HOME + STANDBY
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
// In PLAY specifically, if this session has never gotten a single REAL
|
|
1362
|
-
// (nonzero) VU: frame (!vuSeenReal - the host/platform doesn't support the
|
|
1363
|
-
// visualizer, e.g. macOS today), don't make the user wait out the full
|
|
1364
|
-
// generic idle timer for an animation - drop into the spinning-disc
|
|
1365
|
-
// screensaver as soon as the text-hold window (vuSuppressUntil) expires.
|
|
1366
|
-
// vuSeenReal (not lastVuAt, which now updates on every frame including
|
|
1367
|
-
// all-zero ones) is deliberately a session-level, decided-once fact, so
|
|
1368
|
-
// this can't re-trigger mid-playback and flicker against the bars.
|
|
1369
|
-
bool playSkippingToScreensaver = (uiState == UI_PLAY) && !vuSeenReal &&
|
|
1370
|
-
(long)(millis() - vuSuppressUntil) >= 0;
|
|
1371
|
+
// --- Idle disc screensaver (HOME + STANDBY, or PLAY while paused) ---
|
|
1372
|
+
// Never while a track is actually playing - the track/volume status (or
|
|
1373
|
+
// visualizer) owns the screen then. A static screen is what the power bank
|
|
1374
|
+
// needs to see stay drawing, so the saver covers every idle/paused state.
|
|
1371
1375
|
if (displayOk && !displayBlank &&
|
|
1372
|
-
(uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_PLAY) &&
|
|
1373
|
-
(
|
|
1376
|
+
(uiState == UI_HOME || uiState == UI_STANDBY || (uiState == UI_PLAY && playPaused)) &&
|
|
1377
|
+
(long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
|
|
1374
1378
|
displayBlank = true;
|
|
1375
1379
|
saverStep = 0;
|
|
1376
1380
|
lastSaverFrame = 0;
|
package/discstation.env.example
CHANGED
|
@@ -20,7 +20,7 @@ DISC_DL_OUTPUT_LIMIT_BYTES=8000000000
|
|
|
20
20
|
# ~/.local/share/discstation/tmdb.key
|
|
21
21
|
# DISCSTATION_TMDB_API_KEY=
|
|
22
22
|
# YTDLP_COOKIES_FROM_BROWSER=firefox
|
|
23
|
-
# YTDLP_COOKIES
|
|
23
|
+
# YTDLP_COOKIES=~/.config/discstation/youtube-cookies.txt
|
|
24
24
|
# YTDLP_USER_AGENT=Mozilla/5.0 ...
|
|
25
25
|
# YTDLP_PO_TOKEN=web_embedded.gvs+TOKEN
|
|
26
26
|
# YTDLP_EXTRACTOR_ARGS=youtube:player_client=web_embedded
|
package/docs/PLATFORM_SUPPORT.md
CHANGED
|
@@ -27,37 +27,19 @@ Set `DISC_DEVICE` if automatic drive detection fails. The one known limitation
|
|
|
27
27
|
is **audio-CD *burning*** — `cdrdao` is the only option and often cannot claim
|
|
28
28
|
the drive on recent macOS; DiscStation reports this clearly instead of hanging.
|
|
29
29
|
|
|
30
|
-
### ESP32 remote: OLED spectrum visualizer —
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
app
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`ffmpeg` with no change.
|
|
44
|
-
|
|
45
|
-
The legitimate modern API for this (CoreAudio's Process Tap,
|
|
46
|
-
`AudioHardwareCreateProcessTap`, macOS 14.2+) requires a properly
|
|
47
|
-
code-signed app bundle requesting a specific entitlement, with its own
|
|
48
|
-
dedicated system consent dialog — a bare ad-hoc-signed Homebrew CLI binary
|
|
49
|
-
like `ffmpeg` structurally cannot satisfy that, regardless of which Privacy
|
|
50
|
-
& Security toggles are flipped. Making the visualizer work on macOS would
|
|
51
|
-
mean building a small signed helper app around that API — a real, separate
|
|
52
|
-
project, not started.
|
|
53
|
-
|
|
54
|
-
**Everything else works normally** — audio-CD/DVD/video playback, ripping,
|
|
55
|
-
burning, and the web UI are all unaffected. PLAY just always shows the
|
|
56
|
-
normal text status screen on macOS instead of ever switching to bars.
|
|
57
|
-
|
|
58
|
-
If BlackHole is already installed from an earlier attempt at this, it's
|
|
59
|
-
harmless to leave in place — it just won't do anything useful for
|
|
60
|
-
DiscStation until/unless the above gets built.
|
|
30
|
+
### ESP32 remote: OLED spectrum visualizer — Linux only
|
|
31
|
+
|
|
32
|
+
The bars need a way to tap the audio being played. On Linux that's PulseAudio's
|
|
33
|
+
monitor source (`parec`). macOS has no equivalent a plain CLI tool can use: a
|
|
34
|
+
capture through a loopback driver (BlackHole + `ffmpeg -f avfoundation`) was
|
|
35
|
+
built and verified end to end on real hardware, and returns pure silence
|
|
36
|
+
(every sample `0`, no errors) even while the native Sound input meter shows
|
|
37
|
+
signal — macOS's system-audio-capture privacy protection mutes untrusted
|
|
38
|
+
capturers, and no Privacy & Security toggle changes that. The supported route
|
|
39
|
+
is CoreAudio's Process Tap (`AudioHardwareCreateProcessTap`, macOS 14.2+),
|
|
40
|
+
which needs a code-signed app bundle with its own consent dialog — a separate
|
|
41
|
+
project, not started, so the capture code was removed. PLAY just shows the
|
|
42
|
+
normal text status screen on macOS; everything else is unaffected.
|
|
61
43
|
|
|
62
44
|
### Troubleshooting playback ("mpv not found" / play fails to start)
|
|
63
45
|
|
package/install-macos.sh
CHANGED
|
@@ -28,7 +28,7 @@ if ! command -v brew >/dev/null 2>&1; then
|
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
30
|
brew install python ffmpeg cdrdao dvdauthor node yt-dlp xorriso mpv libdiscid handbrake \
|
|
31
|
-
libdvdcss dvdbackup libcdio-paranoia
|
|
31
|
+
libdvdcss dvdbackup libcdio-paranoia
|
|
32
32
|
mkdir -p "$APP_DIR" "$VENV_DIR" "$CONFIG_DIR"
|
|
33
33
|
cp -R "$ROOT_DIR/src/." "$APP_DIR/"
|
|
34
34
|
python3 -m venv "$VENV_DIR"
|
|
@@ -40,12 +40,9 @@ else
|
|
|
40
40
|
fi
|
|
41
41
|
# Optional metadata deps — best effort. python-libdiscid (a C extension) is
|
|
42
42
|
# Linux-only in requirements-optional.txt; on macOS install the pure-Python
|
|
43
|
-
# bits and let brew's libdiscid cover disc IDs via the CLI tools.
|
|
44
|
-
# for the OLED spectrum visualizer (needs the blackhole-2ch brew package
|
|
45
|
-
# above too, plus a one-time Multi-Output Device setup - see
|
|
46
|
-
# docs/PLATFORM_SUPPORT.md).
|
|
43
|
+
# bits and let brew's libdiscid cover disc IDs via the CLI tools.
|
|
47
44
|
if [[ -f "$ROOT_DIR/requirements-optional.txt" ]]; then
|
|
48
|
-
"$VENV_DIR/bin/python" -m pip install musicbrainzngs tmdbsimple
|
|
45
|
+
"$VENV_DIR/bin/python" -m pip install musicbrainzngs tmdbsimple \
|
|
49
46
|
|| printf 'Optional metadata deps skipped (host still works).\n'
|
|
50
47
|
fi
|
|
51
48
|
|
package/install.sh
CHANGED
|
@@ -14,7 +14,8 @@ fi
|
|
|
14
14
|
if command -v apt-get >/dev/null 2>&1; then
|
|
15
15
|
sudo apt-get update
|
|
16
16
|
sudo apt-get install -y \
|
|
17
|
-
cdrdao dvdauthor ffmpeg genisoimage growisofs handbrake-cli
|
|
17
|
+
cdrdao dvdauthor dvdbackup eject ffmpeg genisoimage growisofs handbrake-cli \
|
|
18
|
+
lsdvd mpv sg3-utils \
|
|
18
19
|
nodejs openssl python3 python3-pip python3-serial python3-mutagen \
|
|
19
20
|
python3-requests python3-pyudev python3-libdiscid python3-musicbrainzngs \
|
|
20
21
|
python3-numpy pulseaudio-utils libdiscid0 python3-venv wodim
|
|
@@ -51,6 +52,10 @@ if [[ ! -f "$CONFIG_DIR/server.crt" || ! -f "$CONFIG_DIR/server.key" ]]; then
|
|
|
51
52
|
fi
|
|
52
53
|
|
|
53
54
|
sudo usermod -aG dialout,cdrom "$USER" || true
|
|
55
|
+
# Don't let the kernel re-close an ejected tray whenever some process (a desktop
|
|
56
|
+
# volume monitor, a disc probe) opens the drive.
|
|
57
|
+
echo 'dev.cdrom.autoclose = 0' | sudo tee /etc/sysctl.d/90-discstation.conf >/dev/null \
|
|
58
|
+
&& sudo sysctl -q -w dev.cdrom.autoclose=0 || true
|
|
54
59
|
mkdir -p "$HOME/.config/systemd/user"
|
|
55
60
|
cp "$ROOT_DIR/systemd/discstation.service" "$HOME/.config/systemd/user/discstation.service"
|
|
56
61
|
systemctl --user daemon-reload
|
package/package.json
CHANGED
|
@@ -13,10 +13,8 @@ python-libdiscid>=2.0 ; sys_platform == "linux"
|
|
|
13
13
|
musicbrainzngs>=0.7.1
|
|
14
14
|
tmdbsimple>=2.9
|
|
15
15
|
|
|
16
|
-
# OLED spectrum visualizer during playback
|
|
17
|
-
# (pulseaudio-utils apt package) - prefer python3-numpy via apt
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
# there since install-macos.sh's venv isn't externally-managed.
|
|
22
|
-
numpy>=1.24 ; sys_platform == "linux" or sys_platform == "darwin"
|
|
16
|
+
# OLED spectrum visualizer during playback (Linux only). Needs `parec`/`pactl`
|
|
17
|
+
# (pulseaudio-utils apt package) - prefer python3-numpy via apt, pip installs
|
|
18
|
+
# are blocked on a system Python. macOS can't capture system audio from a CLI
|
|
19
|
+
# tool - see docs/PLATFORM_SUPPORT.md.
|
|
20
|
+
numpy>=1.24 ; sys_platform == "linux"
|
package/scripts/check.sh
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Static sanity checks for every language in the repo. Run before committing.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
cd "$(dirname "$0")/.."
|
|
5
|
+
|
|
6
|
+
python3 -m py_compile src/*.py
|
|
7
|
+
for f in install.sh install-macos.sh scripts/*.sh; do bash -n "$f"; done
|
|
8
|
+
for f in scripts/*.mjs scripts/lib/*.mjs src/static/app.js; do node --check "$f"; done
|
|
9
|
+
(cd mobile && ./node_modules/.bin/tsc --noEmit --noUnusedLocals)
|
|
10
|
+
|
|
11
|
+
# Import the host and exercise the pure status/menu logic with canned input.
|
|
12
|
+
(cd src && python3 - <<'EOF'
|
|
13
|
+
import discstation as d
|
|
14
|
+
d._record_web_status("PLAY_MODE:AUDIO_CD")
|
|
15
|
+
assert d._status_snapshot()["playing"] is True
|
|
16
|
+
d._record_web_status("PROGRESS:42%")
|
|
17
|
+
assert d._status_snapshot()["progress"] == 42
|
|
18
|
+
d._record_web_status("DONE:ok")
|
|
19
|
+
assert d._status_snapshot()["playing"] is False
|
|
20
|
+
|
|
21
|
+
# Burn-flow helpers: history recording for every exit path, START/SPEED/MODE/CANCEL.
|
|
22
|
+
import json, pathlib, tempfile
|
|
23
|
+
d.HISTORY_FILE = pathlib.Path(tempfile.mkdtemp()) / "h.jsonl"
|
|
24
|
+
with d._burn_history({"title": "t"}):
|
|
25
|
+
pass
|
|
26
|
+
try:
|
|
27
|
+
with d._burn_history({"title": "t"}):
|
|
28
|
+
raise RuntimeError("boom")
|
|
29
|
+
except RuntimeError:
|
|
30
|
+
pass
|
|
31
|
+
with d._burn_history({"title": "t"}, swallow_cancel=True) as h:
|
|
32
|
+
raise d.CancelError("x")
|
|
33
|
+
assert h.cancelled
|
|
34
|
+
rows = [json.loads(l) for l in open(d.HISTORY_FILE)]
|
|
35
|
+
assert [r["success"] for r in rows] == [True, False, False] and rows[1]["error"] == "boom"
|
|
36
|
+
ser = d.VirtualSerial()
|
|
37
|
+
for line in ("SPEED:6x", "START"):
|
|
38
|
+
ser.push_line(line)
|
|
39
|
+
assert d._wait_for_start(ser, "audio burn") == (None, "6x")
|
|
40
|
+
for line in ("MODE:BEST", "START"):
|
|
41
|
+
ser.push_line(line)
|
|
42
|
+
assert d._wait_for_start(ser, mode="AUTO") == ("BEST", None)
|
|
43
|
+
ser.push_line("CANCEL")
|
|
44
|
+
assert d._wait_for_start(ser, "x") is None
|
|
45
|
+
|
|
46
|
+
# Subprocess-output iterators (shared reader thread) + cancel handling.
|
|
47
|
+
import subprocess, sys
|
|
48
|
+
def spawn(code):
|
|
49
|
+
return subprocess.Popen([sys.executable, "-c", code], stdout=subprocess.PIPE, text=True)
|
|
50
|
+
assert list(d.discstation_burn.iter_proc_or_cancel(spawn("print('a');print('b')"), ser)) == ["a", "b"]
|
|
51
|
+
assert [e for e in d.iter_process_events(spawn("print('x')"), idle_seconds=0.2, ser=ser) if e] == ["x"]
|
|
52
|
+
ser.push_line("CANCEL")
|
|
53
|
+
try:
|
|
54
|
+
list(d.iter_process_events(spawn("import time;time.sleep(30)"), idle_seconds=0.2, ser=ser))
|
|
55
|
+
raise SystemExit("cancel not raised")
|
|
56
|
+
except d.CancelError:
|
|
57
|
+
pass
|
|
58
|
+
print("smoke ok")
|
|
59
|
+
EOF
|
|
60
|
+
)
|
|
61
|
+
echo "all checks passed"
|