discstation 0.1.21 → 0.1.23
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/package.json +3 -2
- package/scripts/lib/net.mjs +14 -0
- package/scripts/open.mjs +2 -0
- package/scripts/postinstall.mjs +82 -0
- package/scripts/setup.mjs +4 -1
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
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discstation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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,14 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
|
|
3
|
+
/** First non-internal IPv4 LAN address, so other devices on the network know
|
|
4
|
+
* what to open — mirrors discstation.py's local_ip(). Falls back to
|
|
5
|
+
* 'localhost' if nothing's found (e.g. no network connection). */
|
|
6
|
+
export function lanIp() {
|
|
7
|
+
const nets = networkInterfaces();
|
|
8
|
+
for (const name of Object.keys(nets)) {
|
|
9
|
+
for (const net of nets[name] || []) {
|
|
10
|
+
if (net.family === 'IPv4' && !net.internal) return net.address;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return 'localhost';
|
|
14
|
+
}
|
package/scripts/open.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { get } from 'node:http';
|
|
8
|
+
import { lanIp } from './lib/net.mjs';
|
|
8
9
|
|
|
9
10
|
const port = process.env.DISCSTATION_HTTP_PORT || '8081';
|
|
10
11
|
const url = process.argv[2] || `http://localhost:${port}/`;
|
|
@@ -24,6 +25,7 @@ function open(target) {
|
|
|
24
25
|
// Best-effort liveness check so a stopped host gives a clear hint.
|
|
25
26
|
const probe = get(url, { timeout: 1500 }, (res) => {
|
|
26
27
|
res.resume();
|
|
28
|
+
console.log(`Also reachable at http://${lanIp()}:${port}/ from any phone/computer on your network.`);
|
|
27
29
|
open(url);
|
|
28
30
|
});
|
|
29
31
|
probe.on('timeout', () => probe.destroy());
|
|
@@ -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
|
+
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { lanIp } from './lib/net.mjs';
|
|
11
12
|
|
|
12
13
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -45,9 +46,11 @@ function run(cmd, cmdArgs) {
|
|
|
45
46
|
}
|
|
46
47
|
if ((r.status ?? 1) === 0) {
|
|
47
48
|
const port = process.env.DISCSTATION_HTTP_PORT || '8081';
|
|
49
|
+
const ip = lanIp();
|
|
48
50
|
console.log(
|
|
49
51
|
`\nDiscStation is installed and running.\n` +
|
|
50
|
-
` Web UI: http
|
|
52
|
+
` Web UI: http://${ip}:${port}/ (run "discstation" to open it any time)\n` +
|
|
53
|
+
` Open that address from any phone/computer on your network too.\n` +
|
|
51
54
|
` Install it as an app from the browser menu, or the in-page INSTALL APP button.\n`,
|
|
52
55
|
);
|
|
53
56
|
spawnSync(process.execPath, [join(ROOT, 'scripts', 'open.mjs')], { stdio: 'ignore' });
|