nixamp 0.7.25 → 0.7.28
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 +12 -0
- package/dist/admin.d.ts +10 -0
- package/dist/admin.js +23 -4
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +14 -1
- package/dist/jingle.d.ts +45 -0
- package/dist/jingle.js +129 -0
- package/dist/main.js +10 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +13 -0
- package/package.json +1 -1
- package/src/admin.ts +34 -4
- package/src/daemon.ts +22 -1
- package/src/jingle.ts +147 -0
- package/src/main.ts +9 -0
- package/src/server.ts +14 -0
- package/web/dist/assets/{hls-3VKVEQE3-D-TeBz8d.js → hls-3VKVEQE3-RAqUAHqL.js} +1 -1
- package/web/dist/assets/index-Dkxwdc47.js +1 -0
- package/web/dist/assets/{mpegts-melX63gT.js → mpegts-BofjrHlp.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-Crs76Vy-.js → mpegts-LO6RVLD6-DrXrIFtY.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/jingles/01.mp3 +0 -0
- package/web/dist/jingles/02.mp3 +0 -0
- package/web/dist/jingles/index.json +4 -0
- package/web/dist/sw.js +8 -5
- package/web/dist/assets/index-Z0E29INh.js +0 -1
package/README.md
CHANGED
|
@@ -363,6 +363,18 @@ until you restart it.
|
|
|
363
363
|
An added block can be taken back out from the playlist itself: its heading
|
|
364
364
|
carries an `×`.
|
|
365
365
|
|
|
366
|
+
## The noise it makes
|
|
367
|
+
|
|
368
|
+
nixamp plays a jingle when it starts, once per session, the way Winamp did.
|
|
369
|
+
Any mp3 in your home directory with `nixamp` in its name is used instead of the
|
|
370
|
+
ones that ship, and with more than one it picks at random -- a rotation you can
|
|
371
|
+
predict is one you stop hearing. `--no-jingle`, or `NIXAMP_NO_JINGLE=1`, turns
|
|
372
|
+
it off.
|
|
373
|
+
|
|
374
|
+
On the web it plays on a fresh page if the browser allows it, and otherwise on
|
|
375
|
+
your first click -- browsers spent a decade learning to refuse pages that make
|
|
376
|
+
noise unasked, and this one does not argue with them.
|
|
377
|
+
|
|
366
378
|
## How it works
|
|
367
379
|
|
|
368
380
|
One decode feeds both your speakers and the display. `ffmpeg` writes raw 32-bit float samples to a pipe; nixamp reads every sample on its way past, runs an FFT over it, and hands the same bytes to `ffplay`.
|
package/dist/admin.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface AdminOptions {
|
|
|
39
39
|
label: string;
|
|
40
40
|
url: string;
|
|
41
41
|
}[];
|
|
42
|
+
/**
|
|
43
|
+
* The key that only watches, when this server has one.
|
|
44
|
+
*
|
|
45
|
+
* The links built from it are the ones to hand other people. Absent when
|
|
46
|
+
* pointed at a server by hand, since a listening key is not something you
|
|
47
|
+
* can work out from a control one.
|
|
48
|
+
*/
|
|
49
|
+
listenKey?: string | null;
|
|
42
50
|
/** What it is serving, so the admin view says so without being asked. */
|
|
43
51
|
source: string;
|
|
44
52
|
}
|
|
@@ -76,6 +84,8 @@ export interface View {
|
|
|
76
84
|
url: string;
|
|
77
85
|
}[];
|
|
78
86
|
key: string | null;
|
|
87
|
+
/** The key that only watches, for the links you hand to other people. */
|
|
88
|
+
listenKey?: string | null;
|
|
79
89
|
source: string;
|
|
80
90
|
}
|
|
81
91
|
export declare function draw(ui: Container, theme: Theme, view: View): void;
|
package/dist/admin.js
CHANGED
|
@@ -39,6 +39,7 @@ export function resolveTarget(argv) {
|
|
|
39
39
|
return {
|
|
40
40
|
url: url_,
|
|
41
41
|
key: key ?? state.key,
|
|
42
|
+
...(state.listenKey ? { listenKey: state.listenKey } : {}),
|
|
42
43
|
// A state file written before 0.5.3 has no list; loopback stands in.
|
|
43
44
|
links: state.urls ?? [{ label: "here", url: daemonUrl(state) }],
|
|
44
45
|
source: state.source,
|
|
@@ -176,7 +177,7 @@ export async function admin(argv) {
|
|
|
176
177
|
app.on("exit", () => clearInterval(timer));
|
|
177
178
|
app.render(({ ui, theme }) => draw(ui, theme, {
|
|
178
179
|
url: target.url, report, snapshot, error, typing, restreaming, replacing,
|
|
179
|
-
links: target.links, key: target.key, source: target.source,
|
|
180
|
+
links: target.links, key: target.key, listenKey: target.listenKey, source: target.source,
|
|
180
181
|
}));
|
|
181
182
|
await app.start();
|
|
182
183
|
clearInterval(timer);
|
|
@@ -229,13 +230,31 @@ export function draw(ui, theme, view) {
|
|
|
229
230
|
// reach it. The key is on them: without it every address is a 401.
|
|
230
231
|
if (view.links.length > 0) {
|
|
231
232
|
const width = Math.max(...view.links.map((link) => link.label.length));
|
|
232
|
-
|
|
233
|
+
// Both kinds, labelled, because they are not interchangeable and the
|
|
234
|
+
// operator had only ever been shown the one that drives. Handing that to
|
|
235
|
+
// somebody who wanted to watch hands them the controls with it, and
|
|
236
|
+
// there was nothing else to hand them.
|
|
237
|
+
// Counted rather than guessed: a heading of its own, the links, the second
|
|
238
|
+
// heading and its links when there is a viewing key, the source, and the
|
|
239
|
+
// two borders. A panel one line short silently drops the last row.
|
|
240
|
+
const rows = 1 + view.links.length
|
|
241
|
+
+ (view.listenKey ? 1 + view.links.length : 0)
|
|
242
|
+
+ (view.source ? 1 : 0);
|
|
243
|
+
ui.panel({ title: "Your links, and the ones to share", size: rows + 2 }, (p) => {
|
|
244
|
+
p.label("Yours -- these administer this server:");
|
|
233
245
|
for (const link of view.links) {
|
|
234
|
-
|
|
235
|
-
p.text(`${link.label.padEnd(width)} ${full}`, {
|
|
246
|
+
p.text(`${link.label.padEnd(width)} ${shareLink(link.url, view.key)}`, {
|
|
236
247
|
fg: link.label === "on the internet" ? theme.accent : theme.foreground,
|
|
237
248
|
});
|
|
238
249
|
}
|
|
250
|
+
if (view.listenKey) {
|
|
251
|
+
p.label("To share -- these watch and listen, and change nothing:");
|
|
252
|
+
for (const link of view.links) {
|
|
253
|
+
p.text(`${link.label.padEnd(width)} ${shareLink(link.url, view.listenKey, false)}`, {
|
|
254
|
+
fg: link.label === "on the internet" ? theme.accent : theme.foreground,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
239
258
|
if (view.source)
|
|
240
259
|
p.label(view.source);
|
|
241
260
|
});
|
package/dist/daemon.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export interface DaemonState {
|
|
|
4
4
|
port: number;
|
|
5
5
|
/** The share key, so `nixamp admin` can talk to it without being told. */
|
|
6
6
|
key: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* The key that only views, for handing to somebody who should watch.
|
|
9
|
+
*
|
|
10
|
+
* Absent on a state file written by an older nixamp, and absent on a server
|
|
11
|
+
* running with no key at all.
|
|
12
|
+
*/
|
|
13
|
+
listenKey?: string | null;
|
|
7
14
|
source: string;
|
|
8
15
|
startedAt: number;
|
|
9
16
|
/** Where its output went, for when it died and you want to know why. */
|
package/dist/daemon.js
CHANGED
|
@@ -121,8 +121,11 @@ function spell(ms) {
|
|
|
121
121
|
* typed the command.
|
|
122
122
|
*/
|
|
123
123
|
export function daemonLines(state, uptimeMs) {
|
|
124
|
-
//
|
|
124
|
+
// Two links per address: the one that drives this server and the one that
|
|
125
|
+
// only watches it. Printing just the first left the operator with nothing to
|
|
126
|
+
// hand anybody -- and handing over the wrong one hands over the controls.
|
|
125
127
|
const link = (url) => shareLink(url, state.key ?? null);
|
|
128
|
+
const viewLink = (url) => shareLink(url, state.listenKey ?? null, false);
|
|
126
129
|
// A state file written by an older nixamp has no list, so host and port
|
|
127
130
|
// still stand in rather than printing nothing at all.
|
|
128
131
|
const addresses = state.urls ?? [{ label: "here", url: daemonUrl(state) }];
|
|
@@ -131,6 +134,15 @@ export function daemonLines(state, uptimeMs) {
|
|
|
131
134
|
for (const { label, url } of addresses)
|
|
132
135
|
lines.push(` ${label.padEnd(width)} ${link(url)}`);
|
|
133
136
|
lines.push(` ${"source".padEnd(width)} ${state.source}`);
|
|
137
|
+
// The viewing links, said separately and said as what they are. Somebody who
|
|
138
|
+
// should be able to watch your library should not be handed the link that
|
|
139
|
+
// re-streams it, and the operator had nothing else to give them.
|
|
140
|
+
if (state.listenKey) {
|
|
141
|
+
lines.push("", " To share -- these watch and listen, and change nothing:");
|
|
142
|
+
for (const { label, url } of addresses) {
|
|
143
|
+
lines.push(` ${label.padEnd(width)} ${viewLink(url)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
134
146
|
if (uptimeMs !== undefined)
|
|
135
147
|
lines.push(` ${"up".padEnd(width)} ${spell(uptimeMs)}`);
|
|
136
148
|
if (state.guessedPublic) {
|
|
@@ -227,6 +239,7 @@ async function waitForAnnounce(log, timeoutMs) {
|
|
|
227
239
|
host: String(parsed["host"]),
|
|
228
240
|
port: Number(parsed["port"]),
|
|
229
241
|
key: parsed["key"] ?? null,
|
|
242
|
+
...(typeof parsed["listenKey"] === "string" ? { listenKey: parsed["listenKey"] } : {}),
|
|
230
243
|
source: String(parsed["source"]),
|
|
231
244
|
...(Array.isArray(urls) ? { urls: urls } : {}),
|
|
232
245
|
...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
|
package/dist/jingle.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Tools } from "./audio.ts";
|
|
2
|
+
/** Turned off by `--no-jingle` or by setting this in the environment. */
|
|
3
|
+
export declare const OFF = "NIXAMP_NO_JINGLE";
|
|
4
|
+
/**
|
|
5
|
+
* Every jingle available, yours first, in a stable order.
|
|
6
|
+
*
|
|
7
|
+
* The match is deliberately loose -- any mp3 whose name contains "nixamp" --
|
|
8
|
+
* because the point is that a file you drop in your home directory is picked
|
|
9
|
+
* up, not that you named it exactly right. `001. NixAmp Whips the D-M-C-As.mp3`
|
|
10
|
+
* is a name a person actually uses, and an anchored pattern missed it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findJingles(home?: string, packaged?: string[], read?: (dir: string) => string[]): string[];
|
|
13
|
+
/**
|
|
14
|
+
* One to play, chosen at random.
|
|
15
|
+
*
|
|
16
|
+
* Random rather than in turn: a rotation you can predict is one you stop
|
|
17
|
+
* hearing, and there is no state worth keeping between runs for this.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findJingle(home?: string, packaged?: string[], read?: (dir: string) => string[], pick?: (upTo: number) => number): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Play it, without making anybody wait for it.
|
|
22
|
+
*
|
|
23
|
+
* Detached and unwatched: a jingle that delays the player, or that fails
|
|
24
|
+
* loudly because a codec is missing, is worse than no jingle. Nothing here is
|
|
25
|
+
* awaited and nothing it does can stop a start.
|
|
26
|
+
*/
|
|
27
|
+
export declare function playJingle(tools: Tools, path?: string | null,
|
|
28
|
+
/** Injected so a test can see what would be run on a machine with speakers. */
|
|
29
|
+
start?: Spawner, env?: Record<string, string | undefined>): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Play it and stop, with no window.
|
|
32
|
+
*
|
|
33
|
+
* `-nodisp` because a jingle is not something to open a window for, and
|
|
34
|
+
* `-autoexit` because a player that lingers after the sound is a process
|
|
35
|
+
* somebody has to notice and kill.
|
|
36
|
+
*/
|
|
37
|
+
export declare const JINGLE_ARGS: readonly ["-autoexit", "-nodisp", "-loglevel", "quiet"];
|
|
38
|
+
type Spawner = (command: string, args: string[], options: {
|
|
39
|
+
stdio: "ignore";
|
|
40
|
+
detached: boolean;
|
|
41
|
+
}) => {
|
|
42
|
+
on: (event: "error", run: () => void) => unknown;
|
|
43
|
+
unref?: () => void;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
package/dist/jingle.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The noise nixamp makes when it wakes up.
|
|
3
|
+
*
|
|
4
|
+
* Winamp had one, and it is half of why anybody remembers Winamp. This plays
|
|
5
|
+
* once when a player starts and once on a fresh page at nixamp.com, and never
|
|
6
|
+
* again in that session -- a sound you like the first time is a sound you
|
|
7
|
+
* resent the fourth.
|
|
8
|
+
*
|
|
9
|
+
* Yours win: any mp3 in your home directory with "nixamp" in its name, so you
|
|
10
|
+
* can drop one in without touching anything. Otherwise the ones that ship are
|
|
11
|
+
* used, so it works on a machine that has never heard of any of this.
|
|
12
|
+
*
|
|
13
|
+
* With more than one, it picks at random rather than cycling, because a
|
|
14
|
+
* rotation you can predict is one you stop hearing.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
/** Turned off by `--no-jingle` or by setting this in the environment. */
|
|
22
|
+
export const OFF = "NIXAMP_NO_JINGLE";
|
|
23
|
+
/**
|
|
24
|
+
* Every jingle available, yours first, in a stable order.
|
|
25
|
+
*
|
|
26
|
+
* The match is deliberately loose -- any mp3 whose name contains "nixamp" --
|
|
27
|
+
* because the point is that a file you drop in your home directory is picked
|
|
28
|
+
* up, not that you named it exactly right. `001. NixAmp Whips the D-M-C-As.mp3`
|
|
29
|
+
* is a name a person actually uses, and an anchored pattern missed it.
|
|
30
|
+
*/
|
|
31
|
+
export function findJingles(home = homedir(), packaged = packagedJingles(), read = safeRead) {
|
|
32
|
+
// Guarded here rather than only inside the default reader: whether a jingle
|
|
33
|
+
// can be found is this function's promise to keep, and it should not depend
|
|
34
|
+
// on which reader it was handed.
|
|
35
|
+
let names;
|
|
36
|
+
try {
|
|
37
|
+
names = read(home);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
names = [];
|
|
41
|
+
}
|
|
42
|
+
const mine = names
|
|
43
|
+
.filter((name) => name.toLowerCase().endsWith(".mp3") && name.toLowerCase().includes("nixamp"))
|
|
44
|
+
.sort()
|
|
45
|
+
.map((name) => join(home, name));
|
|
46
|
+
return mine.length > 0 ? mine : packaged;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One to play, chosen at random.
|
|
50
|
+
*
|
|
51
|
+
* Random rather than in turn: a rotation you can predict is one you stop
|
|
52
|
+
* hearing, and there is no state worth keeping between runs for this.
|
|
53
|
+
*/
|
|
54
|
+
export function findJingle(home = homedir(), packaged = packagedJingles(), read = safeRead, pick = (upTo) => Math.floor(Math.random() * upTo)) {
|
|
55
|
+
const all = findJingles(home, packaged, read);
|
|
56
|
+
if (all.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
return all[Math.min(all.length - 1, Math.max(0, pick(all.length)))] ?? null;
|
|
59
|
+
}
|
|
60
|
+
function safeRead(dir) {
|
|
61
|
+
return readdirSync(dir);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The ones that ship, read from the list the web build writes.
|
|
65
|
+
*
|
|
66
|
+
* A list rather than a name, so adding another jingle is dropping a file in
|
|
67
|
+
* and rebuilding rather than editing this.
|
|
68
|
+
*/
|
|
69
|
+
function packagedJingles() {
|
|
70
|
+
try {
|
|
71
|
+
// dist/jingle.js -> the package root -> web/dist, which is what `files`
|
|
72
|
+
// in package.json actually ships.
|
|
73
|
+
const dir = fileURLToPath(new URL("../web/dist/jingles", import.meta.url));
|
|
74
|
+
const listed = JSON.parse(readFileSync(join(dir, "index.json"), "utf8"));
|
|
75
|
+
if (!Array.isArray(listed))
|
|
76
|
+
return [];
|
|
77
|
+
return listed
|
|
78
|
+
.filter((name) => typeof name === "string")
|
|
79
|
+
.map((name) => join(dir, name));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Play it, without making anybody wait for it.
|
|
87
|
+
*
|
|
88
|
+
* Detached and unwatched: a jingle that delays the player, or that fails
|
|
89
|
+
* loudly because a codec is missing, is worse than no jingle. Nothing here is
|
|
90
|
+
* awaited and nothing it does can stop a start.
|
|
91
|
+
*/
|
|
92
|
+
export function playJingle(tools, path = findJingle(),
|
|
93
|
+
/** Injected so a test can see what would be run on a machine with speakers. */
|
|
94
|
+
start = spawn, env = process.env) {
|
|
95
|
+
if (path === null)
|
|
96
|
+
return false;
|
|
97
|
+
if (env[OFF])
|
|
98
|
+
return false;
|
|
99
|
+
const play = tools.play;
|
|
100
|
+
if (play === null)
|
|
101
|
+
return false;
|
|
102
|
+
const [command, ...rest] = play;
|
|
103
|
+
if (command === undefined)
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
const child = start(command, [...rest, ...JINGLE_ARGS, path], {
|
|
107
|
+
stdio: "ignore",
|
|
108
|
+
detached: false,
|
|
109
|
+
});
|
|
110
|
+
child.on("error", () => {
|
|
111
|
+
// No player, a file it will not open, or a machine with no sound card at
|
|
112
|
+
// all -- which is most servers. Silence is the fallback, and it is not
|
|
113
|
+
// worth a line of output on every start.
|
|
114
|
+
});
|
|
115
|
+
child.unref?.();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Play it and stop, with no window.
|
|
124
|
+
*
|
|
125
|
+
* `-nodisp` because a jingle is not something to open a window for, and
|
|
126
|
+
* `-autoexit` because a player that lingers after the sound is a process
|
|
127
|
+
* somebody has to notice and kill.
|
|
128
|
+
*/
|
|
129
|
+
export const JINGLE_ARGS = ["-autoexit", "-nodisp", "-loglevel", "quiet"];
|
package/dist/main.js
CHANGED
|
@@ -16,6 +16,7 @@ import { displayName, loadSource, loadTagged } from "./playlist.js";
|
|
|
16
16
|
import { isRemote } from "./sources.js";
|
|
17
17
|
import { DEFAULT_PORT } from "./server.js";
|
|
18
18
|
import { shareLink } from "./share.js";
|
|
19
|
+
import { playJingle } from "./jingle.js";
|
|
19
20
|
const FFT_SIZE = 2048;
|
|
20
21
|
export const BAND_COUNT = 24;
|
|
21
22
|
export function createState(tracks, root, silent) {
|
|
@@ -60,6 +61,10 @@ const HELP = `nixamp — it really whips the terminal's ass.
|
|
|
60
61
|
A source is a directory, a file, an .m3u, an .m3u8, a .pls, or a URL to any
|
|
61
62
|
of those.
|
|
62
63
|
|
|
64
|
+
It plays a jingle when it starts, picked at random. Any mp3 in your home
|
|
65
|
+
directory with nixamp in the name is yours and wins; otherwise the ones that
|
|
66
|
+
ship are used. --no-jingle, or NIXAMP_NO_JINGLE=1, for silence.
|
|
67
|
+
|
|
63
68
|
Options for serve:
|
|
64
69
|
-p, --port N port to listen on (default ${DEFAULT_PORT})
|
|
65
70
|
-h, --host HOST address to bind (default 0.0.0.0, every interface)
|
|
@@ -344,6 +349,11 @@ export async function main() {
|
|
|
344
349
|
const asked = first ?? ".";
|
|
345
350
|
const target = isRemote(asked) ? asked : resolve(asked);
|
|
346
351
|
const tools = detectTools();
|
|
352
|
+
// The noise it makes when it wakes up. Started before the library is walked
|
|
353
|
+
// so it plays over the wait rather than after it, and never awaited: a
|
|
354
|
+
// jingle that delays the player is worse than no jingle.
|
|
355
|
+
if (!rest.includes("--no-jingle"))
|
|
356
|
+
playJingle(tools);
|
|
347
357
|
// Names now, tags later: an ffprobe per file over a large library is minutes
|
|
348
358
|
// of a blank terminal before the player appears. The list is the same list;
|
|
349
359
|
// only the titles arrive late, and they arrive into a player already running.
|
package/dist/server.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface ServeOptions {
|
|
|
31
31
|
key: boolean;
|
|
32
32
|
/** Mint a new share key rather than reusing the one this port had. */
|
|
33
33
|
newKey: boolean;
|
|
34
|
+
/** Start without the noise it makes when it wakes up. */
|
|
35
|
+
noJingle: boolean;
|
|
34
36
|
/**
|
|
35
37
|
* Ask the local firewall to let the port through, and put it back on the way
|
|
36
38
|
* out. Off by default because it changes the machine, not just this process.
|
package/dist/server.js
CHANGED
|
@@ -29,6 +29,7 @@ import { DeviceGrants } from "./device.js";
|
|
|
29
29
|
import { BAD_KEY_LIMIT, callerOf, Guard, SIGN_IN_LIMIT } from "./guard.js";
|
|
30
30
|
import { deviceDonePage, devicePage, exchangeCode, providersFrom, signInFailedPage, SignIn, } from "./oauth.js";
|
|
31
31
|
import { needsAdmin, Owner } from "./owner.js";
|
|
32
|
+
import { playJingle } from "./jingle.js";
|
|
32
33
|
import { stateDir } from "./daemon.js";
|
|
33
34
|
import { readSession } from "./session.js";
|
|
34
35
|
import { Directory, ENDED_TTL_MS, parseAnnouncement } from "./directory.js";
|
|
@@ -70,6 +71,7 @@ export function parseServeArgs(argv) {
|
|
|
70
71
|
media: true,
|
|
71
72
|
key: true,
|
|
72
73
|
newKey: false,
|
|
74
|
+
noJingle: false,
|
|
73
75
|
openPort: false,
|
|
74
76
|
announce: false,
|
|
75
77
|
directory: false,
|
|
@@ -162,6 +164,9 @@ export function parseServeArgs(argv) {
|
|
|
162
164
|
else if (arg === "--new-key") {
|
|
163
165
|
options.newKey = true;
|
|
164
166
|
}
|
|
167
|
+
else if (arg === "--no-jingle") {
|
|
168
|
+
options.noJingle = true;
|
|
169
|
+
}
|
|
165
170
|
else if (arg === "--ingest") {
|
|
166
171
|
options.ingest = true;
|
|
167
172
|
}
|
|
@@ -2607,6 +2612,10 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2607
2612
|
// Empty on purpose. The walk happens below, once the port is open: it is
|
|
2608
2613
|
// the slowest part of starting and nothing about it needs to happen first.
|
|
2609
2614
|
const engine = new PlayerEngine([], root, tools);
|
|
2615
|
+
// A serving machine with speakers is still a player. A headless one has no
|
|
2616
|
+
// ffplay, and playJingle answers that by doing nothing.
|
|
2617
|
+
if (!options.noJingle)
|
|
2618
|
+
playJingle(tools);
|
|
2610
2619
|
const web = options.web !== null ? resolve(options.web) : defaultWebDir();
|
|
2611
2620
|
// The same keys this port used last time, so a link somebody was given
|
|
2612
2621
|
// still works after a restart -- and a server is restarted to pick up a new
|
|
@@ -2962,6 +2971,10 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2962
2971
|
host: options.host,
|
|
2963
2972
|
port,
|
|
2964
2973
|
key,
|
|
2974
|
+
// The other half of the pair. Without it `nixamp admin` can only draw
|
|
2975
|
+
// the links that administer, so the operator has nothing to hand
|
|
2976
|
+
// somebody who should be able to watch and not to drive.
|
|
2977
|
+
listenKey,
|
|
2965
2978
|
source: root,
|
|
2966
2979
|
urls: addresses,
|
|
2967
2980
|
firewall,
|
package/package.json
CHANGED
package/src/admin.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface AdminOptions {
|
|
|
38
38
|
* anybody. These are the ones worth reading off the screen.
|
|
39
39
|
*/
|
|
40
40
|
links: { label: string; url: string }[];
|
|
41
|
+
/**
|
|
42
|
+
* The key that only watches, when this server has one.
|
|
43
|
+
*
|
|
44
|
+
* The links built from it are the ones to hand other people. Absent when
|
|
45
|
+
* pointed at a server by hand, since a listening key is not something you
|
|
46
|
+
* can work out from a control one.
|
|
47
|
+
*/
|
|
48
|
+
listenKey?: string | null;
|
|
41
49
|
/** What it is serving, so the admin view says so without being asked. */
|
|
42
50
|
source: string;
|
|
43
51
|
}
|
|
@@ -76,6 +84,7 @@ export function resolveTarget(argv: string[]): AdminOptions {
|
|
|
76
84
|
return {
|
|
77
85
|
url: url_,
|
|
78
86
|
key: key ?? state.key,
|
|
87
|
+
...(state.listenKey ? { listenKey: state.listenKey } : {}),
|
|
79
88
|
// A state file written before 0.5.3 has no list; loopback stands in.
|
|
80
89
|
links: state.urls ?? [{ label: "here", url: daemonUrl(state) }],
|
|
81
90
|
source: state.source,
|
|
@@ -198,7 +207,7 @@ export async function admin(argv: string[]): Promise<void> {
|
|
|
198
207
|
app.on("exit", () => clearInterval(timer));
|
|
199
208
|
app.render(({ ui, theme }) => draw(ui, theme, {
|
|
200
209
|
url: target.url, report, snapshot, error, typing, restreaming, replacing,
|
|
201
|
-
links: target.links, key: target.key, source: target.source,
|
|
210
|
+
links: target.links, key: target.key, listenKey: target.listenKey, source: target.source,
|
|
202
211
|
}));
|
|
203
212
|
|
|
204
213
|
await app.start();
|
|
@@ -242,6 +251,8 @@ export interface View {
|
|
|
242
251
|
/** Labelled addresses, and the key that makes them work. */
|
|
243
252
|
links: { label: string; url: string }[];
|
|
244
253
|
key: string | null;
|
|
254
|
+
/** The key that only watches, for the links you hand to other people. */
|
|
255
|
+
listenKey?: string | null;
|
|
245
256
|
source: string;
|
|
246
257
|
}
|
|
247
258
|
|
|
@@ -276,13 +287,32 @@ export function draw(ui: Container, theme: Theme, view: View): void {
|
|
|
276
287
|
// reach it. The key is on them: without it every address is a 401.
|
|
277
288
|
if (view.links.length > 0) {
|
|
278
289
|
const width = Math.max(...view.links.map((link) => link.label.length));
|
|
279
|
-
|
|
290
|
+
// Both kinds, labelled, because they are not interchangeable and the
|
|
291
|
+
// operator had only ever been shown the one that drives. Handing that to
|
|
292
|
+
// somebody who wanted to watch hands them the controls with it, and
|
|
293
|
+
// there was nothing else to hand them.
|
|
294
|
+
// Counted rather than guessed: a heading of its own, the links, the second
|
|
295
|
+
// heading and its links when there is a viewing key, the source, and the
|
|
296
|
+
// two borders. A panel one line short silently drops the last row.
|
|
297
|
+
const rows =
|
|
298
|
+
1 + view.links.length
|
|
299
|
+
+ (view.listenKey ? 1 + view.links.length : 0)
|
|
300
|
+
+ (view.source ? 1 : 0);
|
|
301
|
+
ui.panel({ title: "Your links, and the ones to share", size: rows + 2 }, (p) => {
|
|
302
|
+
p.label("Yours -- these administer this server:");
|
|
280
303
|
for (const link of view.links) {
|
|
281
|
-
|
|
282
|
-
p.text(`${link.label.padEnd(width)} ${full}`, {
|
|
304
|
+
p.text(`${link.label.padEnd(width)} ${shareLink(link.url, view.key)}`, {
|
|
283
305
|
fg: link.label === "on the internet" ? theme.accent : theme.foreground,
|
|
284
306
|
});
|
|
285
307
|
}
|
|
308
|
+
if (view.listenKey) {
|
|
309
|
+
p.label("To share -- these watch and listen, and change nothing:");
|
|
310
|
+
for (const link of view.links) {
|
|
311
|
+
p.text(`${link.label.padEnd(width)} ${shareLink(link.url, view.listenKey, false)}`, {
|
|
312
|
+
fg: link.label === "on the internet" ? theme.accent : theme.foreground,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
286
316
|
if (view.source) p.label(view.source);
|
|
287
317
|
});
|
|
288
318
|
}
|
package/src/daemon.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface DaemonState {
|
|
|
18
18
|
port: number;
|
|
19
19
|
/** The share key, so `nixamp admin` can talk to it without being told. */
|
|
20
20
|
key: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The key that only views, for handing to somebody who should watch.
|
|
23
|
+
*
|
|
24
|
+
* Absent on a state file written by an older nixamp, and absent on a server
|
|
25
|
+
* running with no key at all.
|
|
26
|
+
*/
|
|
27
|
+
listenKey?: string | null;
|
|
21
28
|
source: string;
|
|
22
29
|
startedAt: number;
|
|
23
30
|
/** Where its output went, for when it died and you want to know why. */
|
|
@@ -164,8 +171,11 @@ function spell(ms: number): string {
|
|
|
164
171
|
* typed the command.
|
|
165
172
|
*/
|
|
166
173
|
export function daemonLines(state: DaemonState, uptimeMs?: number): string[] {
|
|
167
|
-
//
|
|
174
|
+
// Two links per address: the one that drives this server and the one that
|
|
175
|
+
// only watches it. Printing just the first left the operator with nothing to
|
|
176
|
+
// hand anybody -- and handing over the wrong one hands over the controls.
|
|
168
177
|
const link = (url: string): string => shareLink(url, state.key ?? null);
|
|
178
|
+
const viewLink = (url: string): string => shareLink(url, state.listenKey ?? null, false);
|
|
169
179
|
// A state file written by an older nixamp has no list, so host and port
|
|
170
180
|
// still stand in rather than printing nothing at all.
|
|
171
181
|
const addresses = state.urls ?? [{ label: "here", url: daemonUrl(state) }];
|
|
@@ -174,6 +184,16 @@ export function daemonLines(state: DaemonState, uptimeMs?: number): string[] {
|
|
|
174
184
|
const lines = [`nixamp daemon running (pid ${state.pid})`];
|
|
175
185
|
for (const { label, url } of addresses) lines.push(` ${label.padEnd(width)} ${link(url)}`);
|
|
176
186
|
lines.push(` ${"source".padEnd(width)} ${state.source}`);
|
|
187
|
+
|
|
188
|
+
// The viewing links, said separately and said as what they are. Somebody who
|
|
189
|
+
// should be able to watch your library should not be handed the link that
|
|
190
|
+
// re-streams it, and the operator had nothing else to give them.
|
|
191
|
+
if (state.listenKey) {
|
|
192
|
+
lines.push("", " To share -- these watch and listen, and change nothing:");
|
|
193
|
+
for (const { label, url } of addresses) {
|
|
194
|
+
lines.push(` ${label.padEnd(width)} ${viewLink(url)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
177
197
|
if (uptimeMs !== undefined) lines.push(` ${"up".padEnd(width)} ${spell(uptimeMs)}`);
|
|
178
198
|
|
|
179
199
|
if (state.guessedPublic) {
|
|
@@ -300,6 +320,7 @@ async function waitForAnnounce(
|
|
|
300
320
|
host: String(parsed["host"]),
|
|
301
321
|
port: Number(parsed["port"]),
|
|
302
322
|
key: (parsed["key"] as string | null) ?? null,
|
|
323
|
+
...(typeof parsed["listenKey"] === "string" ? { listenKey: parsed["listenKey"] } : {}),
|
|
303
324
|
source: String(parsed["source"]),
|
|
304
325
|
...(Array.isArray(urls) ? { urls: urls as { label: string; url: string }[] } : {}),
|
|
305
326
|
...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
|