discstation 0.1.20 → 0.1.22
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 +26 -4
- package/arduino/c6/DiscStation_C6.ino +7 -1
- package/arduino/v1/DiscStation.ino +4 -1
- package/install-windows.ps1 +29 -6
- package/package.json +3 -2
- package/scripts/postinstall.mjs +82 -0
- package/src/discstation.py +409 -83
- package/src/discstation_burn.py +96 -18
- package/src/discstation_host.py +47 -16
- package/src/static/app.js +72 -0
- package/src/static/index.html +37 -3
- package/src/static/style.css +38 -0
- package/src/win/burn-audio.ps1 +18 -3
- package/src/win/burn-data.ps1 +21 -9
- package/src/win/burn-image.ps1 +17 -13
- package/src/win/disc-info.ps1 +13 -8
- package/src/win/play-audio-cd.ps1 +94 -0
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# DiscStation
|
|
2
2
|
|
|
3
3
|
DiscStation is a physical-media appliance for DVD-Video, data DVDs, audio CDs,
|
|
4
|
-
playback, ripping, phone uploads, and ESP32 remote control.
|
|
4
|
+
playback, ripping, phone uploads, and ESP32 remote control. The ESP32 remote
|
|
5
|
+
is optional — with none attached, the built-in web UI becomes a full
|
|
6
|
+
on-screen remote instead, so `npm install -g discstation` alone is enough to
|
|
7
|
+
use every mode.
|
|
5
8
|
|
|
6
9
|
## Hardware
|
|
7
10
|
|
|
8
11
|
- **Brain:** Raspberry Pi 4/5 (or any Linux box)
|
|
9
|
-
- **Remote:** ESP32-C6 with SSD1306 OLED, rotary encoder, and
|
|
12
|
+
- **Remote (optional):** ESP32-C6 with SSD1306 OLED, rotary encoder, and
|
|
13
|
+
buttons — or none at all; the web UI's on-screen remote covers the same
|
|
14
|
+
controls when no ESP32 is detected, and steps aside (greyed out,
|
|
15
|
+
auto-collapsing) the moment one is plugged in
|
|
10
16
|
- **Drive:** ATAPI DVD writer (e.g. iHAS124) over USB
|
|
11
17
|
|
|
12
18
|
## Features
|
|
@@ -58,7 +64,15 @@ get a standalone app window with a dock/taskbar icon on macOS, Linux, and
|
|
|
58
64
|
Windows.
|
|
59
65
|
|
|
60
66
|
`discstation-setup --help` lists the forwarded env vars. From a git clone,
|
|
61
|
-
`npm run setup` does the same thing.
|
|
67
|
+
`npm run setup` does the same thing.
|
|
68
|
+
|
|
69
|
+
**Updating:** `npm install -g discstation` alone now redeploys and restarts
|
|
70
|
+
the already-running service automatically (a `postinstall` hook copies the
|
|
71
|
+
updated files over and restarts it) — no need to re-run `discstation-setup`
|
|
72
|
+
after every update. `discstation-setup` itself is still what performs the
|
|
73
|
+
first install (system packages, venv, cert, service registration).
|
|
74
|
+
|
|
75
|
+
Support by OS:
|
|
62
76
|
|
|
63
77
|
| OS | What runs | Optical support |
|
|
64
78
|
|----|-----------|-----------------|
|
|
@@ -95,11 +109,19 @@ mode but not full VIDEO_TS mirror or audio-CD ripping yet. See
|
|
|
95
109
|
|
|
96
110
|
## Web Interface
|
|
97
111
|
|
|
98
|
-
Built-in web server on port 8080 (HTTPS with self-signed cert)
|
|
112
|
+
Built-in web server on port 8080 (HTTPS with self-signed cert) and a plain
|
|
113
|
+
HTTP mirror on 8081 for the mobile app / any browser that balks at the
|
|
114
|
+
self-signed cert. On startup the host prints the LAN URL to open
|
|
115
|
+
(`>>> Open http://<ip>:8081 ... <<<`).
|
|
99
116
|
|
|
100
117
|
- Upload files from any device on the LAN for data DVD burning
|
|
101
118
|
- Submit YouTube URLs for video DVD burning
|
|
102
119
|
- Dark theme, mobile-responsive, PWA (installable on phone)
|
|
120
|
+
- **On-screen remote** — click the "REMOTE" tag next to the DiscStation
|
|
121
|
+
wordmark to reveal a full control surface (mode buttons, EJECT/CLOSE
|
|
122
|
+
toggle, live disc-status readout, playback transport). Fully functional
|
|
123
|
+
with no ESP32 attached; greys out and auto-collapses the instant a
|
|
124
|
+
physical remote is detected, so the two never fight for control.
|
|
103
125
|
|
|
104
126
|
## Disc Support
|
|
105
127
|
|
|
@@ -510,7 +510,13 @@ void parseMessage(String msg) {
|
|
|
510
510
|
line1 = msg.substring(7);
|
|
511
511
|
line2 = "";
|
|
512
512
|
line3 = "";
|
|
513
|
-
progressPercent
|
|
513
|
+
// Don't reset progressPercent here - a STATUS: label change (e.g.
|
|
514
|
+
// "Copying files..." -> "Converting...") is often immediately
|
|
515
|
+
// followed by a fresh PROGRESS: for the same ongoing operation.
|
|
516
|
+
// Wiping it every time flipped the bar to the indeterminate dots
|
|
517
|
+
// and back on every single label update, flickering between the
|
|
518
|
+
// two. DONE:/CANCELLED:/ERROR:/NO_DISC: still reset it below - those
|
|
519
|
+
// really are the end of an operation.
|
|
514
520
|
drawStatus();
|
|
515
521
|
|
|
516
522
|
} else if (msg.startsWith("PROGRESS:")) {
|
|
@@ -535,7 +535,10 @@ void parseMessage(String msg) {
|
|
|
535
535
|
line1 = msg.substring(7);
|
|
536
536
|
line2 = "";
|
|
537
537
|
line3 = "";
|
|
538
|
-
progressPercent
|
|
538
|
+
// Don't reset progressPercent here - see arduino/c6/DiscStation_C6.ino's
|
|
539
|
+
// matching comment: STATUS: label changes mid-operation are usually
|
|
540
|
+
// immediately followed by a fresh PROGRESS:, so wiping it every time
|
|
541
|
+
// flickered the bar to the indeterminate dots and back.
|
|
539
542
|
drawStatus();
|
|
540
543
|
|
|
541
544
|
} else if (msg.startsWith("PROGRESS:")) {
|
package/install-windows.ps1
CHANGED
|
@@ -64,13 +64,32 @@ if ($winget -and -not $IsWin7) {
|
|
|
64
64
|
catch { Write-Host " yt-dlp fetch skipped" }
|
|
65
65
|
}
|
|
66
66
|
if (-not (Have "ffmpeg")) { Get-Zip "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" $tools | Out-Null }
|
|
67
|
-
if (-not (Have "mpv")) {
|
|
67
|
+
if (-not (Have "mpv")) {
|
|
68
|
+
# The SourceForge "latest" mpv build is a .7z (Expand-Archive can't
|
|
69
|
+
# open it); mpv's own first-party CI release ships plain .zip builds.
|
|
70
|
+
try {
|
|
71
|
+
$mpvRelease = Invoke-RestMethod "https://api.github.com/repos/mpv-player/mpv/releases/tags/git-release" -UseBasicParsing -TimeoutSec 25
|
|
72
|
+
$mpvAsset = $mpvRelease.assets | Where-Object { $_.name -match "x86_64-w64-mingw32\.zip$" } | Select-Object -First 1
|
|
73
|
+
if ($mpvAsset) { Get-Zip $mpvAsset.browser_download_url (Join-Path $tools "mpv") | Out-Null }
|
|
74
|
+
} catch { Write-Host " mpv fetch skipped" }
|
|
75
|
+
}
|
|
76
|
+
if (-not (Have "HandBrakeCLI")) {
|
|
77
|
+
try {
|
|
78
|
+
$hbRelease = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" -UseBasicParsing -TimeoutSec 25
|
|
79
|
+
$hbAsset = $hbRelease.assets | Where-Object { $_.name -match "^HandBrakeCLI-.*-win-x86_64\.zip$" } | Select-Object -First 1
|
|
80
|
+
if ($hbAsset) { Get-Zip $hbAsset.browser_download_url (Join-Path $tools "handbrake") | Out-Null }
|
|
81
|
+
} catch { Write-Host " HandBrakeCLI fetch skipped" }
|
|
82
|
+
}
|
|
83
|
+
# dvdauthor + spumux (DVD-Video authoring/subtitles) have no winget package;
|
|
84
|
+
# this VideoHelp-hosted plain .zip (no rar/unrar needed) is the only
|
|
85
|
+
# reliable direct-download source found.
|
|
86
|
+
if (-not (Have "dvdauthor")) { Get-Zip "https://download.videohelp.com/gfd/edcounter.php?file=download/dvdauthor_winbin.zip" (Join-Path $tools "dvdauthor") | Out-Null }
|
|
68
87
|
if (Test-Path $tools) {
|
|
69
88
|
$env:Path = $env:Path + ";" + $tools + ";" +
|
|
70
89
|
((Get-ChildItem $tools -Recurse -Filter "ffmpeg.exe" -ErrorAction SilentlyContinue | Select-Object -First 1).DirectoryName)
|
|
71
90
|
}
|
|
72
91
|
Write-Host "xorriso and HandBrakeCLI have no simple direct-download URL - install manually if you need"
|
|
73
|
-
Write-Host "video-DVD
|
|
92
|
+
Write-Host "video-DVD ISO packaging / DVD ripping (see the plan's Windows setup notes)."
|
|
74
93
|
} else {
|
|
75
94
|
Write-Host "winget unavailable (Windows 7). ISO + data + audio burn work via IMAPI2."
|
|
76
95
|
Write-Host "For rip/play/video-DVD install manually: xorriso, HandBrakeCLI 1.5.1, ffmpeg, mpv, yt-dlp."
|
|
@@ -126,14 +145,18 @@ try {
|
|
|
126
145
|
$action = New-ScheduledTaskAction -Execute $pyw -Argument "`"$target`"" -WorkingDirectory $App
|
|
127
146
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
128
147
|
$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
148
|
+
# Interactive: attaches to the logged-in desktop session, same
|
|
149
|
+
# lifecycle as `systemctl --user`/launchd LaunchAgents on the other
|
|
150
|
+
# two platforms - required so PLAY's mpv window actually renders
|
|
151
|
+
# somewhere visible (S4U runs headlessly with no session to render
|
|
152
|
+
# into, which silently made every mpv window invisible). Trade-off,
|
|
153
|
+
# same one Linux/macOS already accept: won't start until someone is
|
|
154
|
+
# logged into the desktop.
|
|
132
155
|
# RunLevel Limited (standard, non-elevated): nothing here needs admin -
|
|
133
156
|
# IMAPI2 burning, WMI reads, and binding ports >1024 all work as a normal
|
|
134
157
|
# user, and elevation is what put "Administrator" on the flashing console
|
|
135
158
|
# windows this used to spawn.
|
|
136
|
-
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType
|
|
159
|
+
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited
|
|
137
160
|
Register-ScheduledTask -TaskName "DiscStation" -Action $action -Trigger $trigger -Settings $set -Principal $principal -Force | Out-Null
|
|
138
161
|
Start-ScheduledTask -TaskName "DiscStation"
|
|
139
162
|
} else {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discstation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "DiscStation optical-media appliance host — one cross-OS installer",
|
|
5
5
|
"bin": {
|
|
6
6
|
"discstation-setup": "scripts/setup.mjs",
|
|
7
7
|
"discstation": "scripts/open.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"setup": "node scripts/setup.mjs"
|
|
10
|
+
"setup": "node scripts/setup.mjs",
|
|
11
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=16"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs automatically after `npm install -g discstation` (fresh install or an
|
|
3
|
+
// update). Closes the class of bug where `npm install -g` updates the
|
|
4
|
+
// package but an already-running background service (systemd/launchd/a
|
|
5
|
+
// Windows Scheduled Task) keeps serving the old code because nobody
|
|
6
|
+
// remembered to separately re-run `discstation-setup` afterward.
|
|
7
|
+
//
|
|
8
|
+
// Deliberately lightweight: only copies files + restarts the service if one
|
|
9
|
+
// is *already* installed. Never touches brew/apt/winget or does a first
|
|
10
|
+
// install - that stays an explicit, visible `discstation-setup` run, since
|
|
11
|
+
// it needs heavier system-package work a silent postinstall shouldn't do.
|
|
12
|
+
import { existsSync, cpSync } from 'node:fs';
|
|
13
|
+
import { homedir, platform } from 'node:os';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
20
|
+
// Never auto-run inside a dev checkout of this repo itself.
|
|
21
|
+
if (existsSync(join(ROOT, '.git'))) process.exit(0);
|
|
22
|
+
// Only for a real global install - a project merely depending on this
|
|
23
|
+
// package shouldn't have a background service silently touched.
|
|
24
|
+
if (!process.env.npm_config_global) process.exit(0);
|
|
25
|
+
|
|
26
|
+
function configDir() {
|
|
27
|
+
if (process.env.DISCSTATION_CONFIG_DIR) return process.env.DISCSTATION_CONFIG_DIR;
|
|
28
|
+
const p = platform();
|
|
29
|
+
if (p === 'win32') return join(process.env.APPDATA || homedir(), 'DiscStation');
|
|
30
|
+
if (p === 'darwin') return join(homedir(), 'Library', 'Application Support', 'DiscStation');
|
|
31
|
+
return join(homedir(), '.local', 'share', 'discstation');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const appDir = process.env.DISCSTATION_APP_DIR || join(configDir(), 'app');
|
|
35
|
+
|
|
36
|
+
function alreadyInstalled() {
|
|
37
|
+
const p = platform();
|
|
38
|
+
if (p === 'darwin') {
|
|
39
|
+
return existsSync(join(homedir(), 'Library', 'LaunchAgents', 'com.discstation.agent.plist'));
|
|
40
|
+
}
|
|
41
|
+
if (p === 'win32') {
|
|
42
|
+
const r = spawnSync('schtasks', ['/Query', '/TN', 'DiscStation'], { stdio: 'ignore' });
|
|
43
|
+
return r.status === 0;
|
|
44
|
+
}
|
|
45
|
+
return existsSync(join(homedir(), '.config', 'systemd', 'user', 'discstation.service'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function restartService() {
|
|
49
|
+
const p = platform();
|
|
50
|
+
try {
|
|
51
|
+
if (p === 'darwin') {
|
|
52
|
+
spawnSync('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/com.discstation.agent`], { stdio: 'ignore' });
|
|
53
|
+
} else if (p === 'win32') {
|
|
54
|
+
spawnSync(
|
|
55
|
+
'powershell.exe',
|
|
56
|
+
[
|
|
57
|
+
'-NoProfile',
|
|
58
|
+
'-Command',
|
|
59
|
+
'Stop-ScheduledTask -TaskName DiscStation -ErrorAction SilentlyContinue; Start-ScheduledTask -TaskName DiscStation',
|
|
60
|
+
],
|
|
61
|
+
{ stdio: 'ignore' }
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
spawnSync('systemctl', ['--user', 'restart', 'discstation.service'], { stdio: 'ignore' });
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort - a failed restart here shouldn't fail the npm install */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!existsSync(appDir) || !alreadyInstalled()) {
|
|
72
|
+
console.log('\ndiscstation: first install detected - run `discstation-setup` to finish setting up the host.\n');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
cpSync(join(ROOT, 'src'), appDir, { recursive: true, force: true });
|
|
78
|
+
restartService();
|
|
79
|
+
console.log('\ndiscstation: redeployed the updated host and restarted the service.\n');
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.log(`\ndiscstation: could not auto-redeploy (${e.message}). Run \`discstation-setup\` manually.\n`);
|
|
82
|
+
}
|