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 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 buttons
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. Support by OS:
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 = -1;
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 = -1;
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:")) {
@@ -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")) { Get-Zip "https://sourceforge.net/projects/mpv-player-windows/files/latest/download" $tools | Out-Null }
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 authoring / DVD ripping (see the plan's Windows setup notes)."
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
- # S4U: runs headlessly for this user without needing an active interactive
130
- # desktop session (Interactive logon type only fires while one exists, so
131
- # e.g. Start-ScheduledTask over SSH/no console session would silently no-op).
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 S4U -RunLevel Limited
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.20",
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
+ }