nixamp 0.5.1 → 0.5.2
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 +21 -0
- package/dist/main.js +23 -1
- package/dist/server.d.ts +21 -0
- package/dist/server.js +49 -2
- package/dist/share.d.ts +1 -1
- package/dist/share.js +8 -2
- package/package.json +1 -1
- package/src/main.ts +22 -1
- package/src/server.ts +67 -2
- package/src/share.ts +13 -2
- package/web/dist/sw.js +1 -1
package/README.md
CHANGED
|
@@ -268,6 +268,27 @@ Start writes down where it went and the key it minted, waits until the server
|
|
|
268
268
|
is actually answering before saying it started, and prints the share link. It is
|
|
269
269
|
one daemon per user, and the state lives in `$XDG_STATE_HOME/nixamp`.
|
|
270
270
|
|
|
271
|
+
### Somewhere the rest of the world can reach
|
|
272
|
+
|
|
273
|
+
The addresses nixamp prints are the ones its own interfaces have, so a machine
|
|
274
|
+
behind NAT only ever sees `192.168.x` -- no use to anybody else, and nothing it
|
|
275
|
+
can publish. Tell it the address it answers on from outside:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
nixamp serve ~/Music --public-url https://nixamp.example.com # or NIXAMP_PUBLIC_URL
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
That address is what the share links print and what the directory listing
|
|
282
|
+
carries. Getting one is your business, not nixamp's: a forwarded port, a reverse
|
|
283
|
+
proxy, or a tunnel, e.g.
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
cloudflared tunnel --url http://localhost:8420
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Without it, `--publish` is skipped entirely rather than listing a stream nobody
|
|
290
|
+
outside the house can open.
|
|
291
|
+
|
|
271
292
|
### Detaching, and coming back
|
|
272
293
|
|
|
273
294
|
`d` in the player hands the music to a daemon and gives you your terminal back.
|
package/dist/main.js
CHANGED
|
@@ -67,6 +67,9 @@ Options for serve:
|
|
|
67
67
|
--publish list it at nixamp.com/directory without asking first
|
|
68
68
|
--no-publish never list it, and do not ask
|
|
69
69
|
--name NAME what to call it in the directory (default: this hostname)
|
|
70
|
+
--public-url URL the address this server is reachable at from outside,
|
|
71
|
+
when that is a tunnel or a forwarded port rather than one of
|
|
72
|
+
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
70
73
|
--ingest accept a live stream in at POST /api/ingest
|
|
71
74
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
72
75
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
|
@@ -300,7 +303,10 @@ export async function main() {
|
|
|
300
303
|
const asked = first ?? ".";
|
|
301
304
|
const target = isRemote(asked) ? asked : resolve(asked);
|
|
302
305
|
const tools = detectTools();
|
|
303
|
-
|
|
306
|
+
// Names now, tags later: an ffprobe per file over a large library is minutes
|
|
307
|
+
// of a blank terminal before the player appears. The list is the same list;
|
|
308
|
+
// only the titles arrive late, and they arrive into a player already running.
|
|
309
|
+
const tracks = await loadSource(tools, target, false);
|
|
304
310
|
if (tracks.length === 0) {
|
|
305
311
|
console.error(`nixamp: no audio files under ${target}`);
|
|
306
312
|
process.exit(1);
|
|
@@ -311,6 +317,22 @@ export async function main() {
|
|
|
311
317
|
// gone. A field rather than a local, because a local assigned only inside a
|
|
312
318
|
// closure stays narrowed to null for the checker.
|
|
313
319
|
const handoff = { to: null };
|
|
320
|
+
// The titles, arriving into a player that is already up. Not awaited, and
|
|
321
|
+
// applied only if the list is still the one it describes.
|
|
322
|
+
if (!isRemote(target)) {
|
|
323
|
+
void loadSource(tools, target, true)
|
|
324
|
+
.then((tagged) => {
|
|
325
|
+
if (tagged.length !== state.tracks.length)
|
|
326
|
+
return;
|
|
327
|
+
if (tagged.some((track, at) => track.path !== state.tracks[at]?.path))
|
|
328
|
+
return;
|
|
329
|
+
state.tracks = tagged;
|
|
330
|
+
app.invalidate();
|
|
331
|
+
})
|
|
332
|
+
.catch(() => {
|
|
333
|
+
// Filenames play. Nothing to say about tags that would not read.
|
|
334
|
+
});
|
|
335
|
+
}
|
|
314
336
|
const analyser = new Analyser(FFT_SIZE, RATE);
|
|
315
337
|
const edges = bandEdges(BAND_COUNT, RATE, FFT_SIZE);
|
|
316
338
|
// Samples accumulate until there are enough for one transform.
|
package/dist/server.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export interface ServeOptions {
|
|
|
47
47
|
publish: "ask" | "yes" | "no";
|
|
48
48
|
/** What to call it in the list. Defaults to this machine's hostname. */
|
|
49
49
|
name: string;
|
|
50
|
+
/**
|
|
51
|
+
* The address this server is reachable at from outside, when that is not one
|
|
52
|
+
* of its own interfaces: a tunnel, a reverse proxy, a forwarded port.
|
|
53
|
+
*
|
|
54
|
+
* Without it a machine behind NAT has nothing to publish -- every address it
|
|
55
|
+
* can see is a 192.168 one that is no use to anybody else -- so the directory
|
|
56
|
+
* listing is skipped and the printed links only work inside the house.
|
|
57
|
+
*/
|
|
58
|
+
publicUrl: string;
|
|
50
59
|
/**
|
|
51
60
|
* Charge for listening once the stream is busy. Off unless asked for, and
|
|
52
61
|
* useless without somewhere to pay: see NIXAMP_PAY_TO.
|
|
@@ -107,6 +116,16 @@ export interface Engine {
|
|
|
107
116
|
* dropping every listener.
|
|
108
117
|
*/
|
|
109
118
|
replace(tracks: Track[], root: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* The same tracks, now with their tags.
|
|
121
|
+
*
|
|
122
|
+
* Startup lists filenames and begins serving immediately, because an ffprobe
|
|
123
|
+
* per file over a real library takes minutes; the tags arrive afterwards and
|
|
124
|
+
* land here. Unlike `replace` this must not disturb anything -- whoever is
|
|
125
|
+
* listening keeps listening, and the only visible change is that the titles
|
|
126
|
+
* fill in.
|
|
127
|
+
*/
|
|
128
|
+
retag(tracks: Track[], root: string): void;
|
|
110
129
|
stop(): void;
|
|
111
130
|
}
|
|
112
131
|
export declare function toRemoteTracks(tracks: Track[]): RemoteTrack[];
|
|
@@ -146,6 +165,7 @@ export declare class PlayerEngine implements Engine {
|
|
|
146
165
|
private push;
|
|
147
166
|
stop(): void;
|
|
148
167
|
replace(tracks: Track[], root: string): void;
|
|
168
|
+
retag(tracks: Track[], root: string): void;
|
|
149
169
|
}
|
|
150
170
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
151
171
|
export declare class EmptyEngine implements Engine {
|
|
@@ -156,6 +176,7 @@ export declare class EmptyEngine implements Engine {
|
|
|
156
176
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
157
177
|
trackPath(): undefined;
|
|
158
178
|
replace(): void;
|
|
179
|
+
retag(): void;
|
|
159
180
|
stop(): void;
|
|
160
181
|
}
|
|
161
182
|
/**
|
package/dist/server.js
CHANGED
|
@@ -66,6 +66,7 @@ export function parseServeArgs(argv) {
|
|
|
66
66
|
directory: false,
|
|
67
67
|
publish: "ask",
|
|
68
68
|
name: "",
|
|
69
|
+
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
69
70
|
x402: false,
|
|
70
71
|
owner: "",
|
|
71
72
|
ingest: false,
|
|
@@ -117,6 +118,15 @@ export function parseServeArgs(argv) {
|
|
|
117
118
|
else if (arg === "--no-publish") {
|
|
118
119
|
options.publish = "no";
|
|
119
120
|
}
|
|
121
|
+
else if (arg === "--public-url") {
|
|
122
|
+
const given = value().trim();
|
|
123
|
+
// A hostname on its own is the likely typo, and it fails much later --
|
|
124
|
+
// as a directory listing nobody can open -- so it is refused here.
|
|
125
|
+
if (!/^https?:\/\/[^\s/]+/i.test(given)) {
|
|
126
|
+
throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
|
|
127
|
+
}
|
|
128
|
+
options.publicUrl = given.replace(/\/+$/, "");
|
|
129
|
+
}
|
|
120
130
|
else if (arg === "--name") {
|
|
121
131
|
options.name = value();
|
|
122
132
|
}
|
|
@@ -444,6 +454,19 @@ export class PlayerEngine {
|
|
|
444
454
|
this.state.note = "";
|
|
445
455
|
this.push();
|
|
446
456
|
}
|
|
457
|
+
retag(tracks, root) {
|
|
458
|
+
// Dropped rather than applied if the library moved underneath: somebody
|
|
459
|
+
// re-streamed while the tagging was still running, and these tags describe
|
|
460
|
+
// something nobody is playing any more.
|
|
461
|
+
if (root !== this.root || tracks.length !== this.tracks.length)
|
|
462
|
+
return;
|
|
463
|
+
if (tracks.some((track, at) => track.path !== this.tracks[at]?.path))
|
|
464
|
+
return;
|
|
465
|
+
this.tracks = tracks;
|
|
466
|
+
// No stop, no index reset: the only thing that changes is what the titles
|
|
467
|
+
// say, and every remote finds out because a snapshot goes out.
|
|
468
|
+
this.push();
|
|
469
|
+
}
|
|
447
470
|
}
|
|
448
471
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
449
472
|
export class EmptyEngine {
|
|
@@ -463,6 +486,7 @@ export class EmptyEngine {
|
|
|
463
486
|
return undefined;
|
|
464
487
|
}
|
|
465
488
|
replace() { }
|
|
489
|
+
retag() { }
|
|
466
490
|
stop() { }
|
|
467
491
|
}
|
|
468
492
|
const CORS = {
|
|
@@ -1804,7 +1828,18 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
1804
1828
|
const options = parseServeArgs(argv);
|
|
1805
1829
|
const root = isRemote(options.root) ? options.root : resolve(options.root);
|
|
1806
1830
|
const tools = detectTools();
|
|
1807
|
-
|
|
1831
|
+
// Names now, tags later.
|
|
1832
|
+
//
|
|
1833
|
+
// Reading tags is an ffprobe per file, which over a real library is minutes,
|
|
1834
|
+
// and every one of them used to happen before this process printed a word or
|
|
1835
|
+
// listened on a port. `nixamp serve ~/music` looked hung, and `nixamp daemon
|
|
1836
|
+
// start` was worse: it waits fifteen seconds for the announce line, killed a
|
|
1837
|
+
// daemon that was working perfectly, and reported a failure whose log was
|
|
1838
|
+
// empty because nothing had been written to it yet.
|
|
1839
|
+
//
|
|
1840
|
+
// So the filenames are enough to start: the server is up and answering in the
|
|
1841
|
+
// time it takes to walk the directory, and the titles fill in behind it.
|
|
1842
|
+
const tracks = await loadSource(tools, root, false);
|
|
1808
1843
|
const engine = tracks.length > 0
|
|
1809
1844
|
? new PlayerEngine(tracks, root, tools)
|
|
1810
1845
|
: new EmptyEngine(`No audio files under ${root}.`);
|
|
@@ -2053,11 +2088,23 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2053
2088
|
if (options.announce) {
|
|
2054
2089
|
console.log(JSON.stringify({ nixamp: "listening", host: options.host, port, key, source: root }));
|
|
2055
2090
|
}
|
|
2091
|
+
// The other half of "names now, tags later". It runs while the banner is
|
|
2092
|
+
// printed and while the publish prompt waits, and it is deliberately not
|
|
2093
|
+
// awaited: nothing downstream needs it, and a library that takes a minute to
|
|
2094
|
+
// read should cost nobody a minute of silence.
|
|
2095
|
+
if (tracks.length > 0 && !isRemote(root)) {
|
|
2096
|
+
void loadSource(tools, root, true)
|
|
2097
|
+
.then((tagged) => engine.retag(tagged, root))
|
|
2098
|
+
.catch(() => {
|
|
2099
|
+
// Filenames are a working player. A failure here is worth nothing but
|
|
2100
|
+
// titles that stay as they are.
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2056
2103
|
console.log(`nixamp serve — ${tracks.length} tracks under ${root}`);
|
|
2057
2104
|
console.log("");
|
|
2058
2105
|
// The link, not the address. Without the key the address is a 401, so
|
|
2059
2106
|
// printing a bare host:port would be printing something that does not work.
|
|
2060
|
-
const addresses = reachableAddresses(options.host, port);
|
|
2107
|
+
const addresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2061
2108
|
const width = Math.max(...addresses.map((a) => a.label.length));
|
|
2062
2109
|
for (const { label, url } of addresses) {
|
|
2063
2110
|
console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
|
package/dist/share.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function classify(address: string): "private" | "cgnat" | "public
|
|
|
31
31
|
* somewhere else can open. It is labelled for what it is, because the key in
|
|
32
32
|
* the link is then the only thing between a stranger and the library.
|
|
33
33
|
*/
|
|
34
|
-
export declare function reachableAddresses(host: string, port: number): {
|
|
34
|
+
export declare function reachableAddresses(host: string, port: number, publicUrl?: string): {
|
|
35
35
|
label: string;
|
|
36
36
|
url: string;
|
|
37
37
|
}[];
|
package/dist/share.js
CHANGED
|
@@ -80,14 +80,19 @@ export function classify(address) {
|
|
|
80
80
|
* somewhere else can open. It is labelled for what it is, because the key in
|
|
81
81
|
* the link is then the only thing between a stranger and the library.
|
|
82
82
|
*/
|
|
83
|
-
export function reachableAddresses(host, port) {
|
|
83
|
+
export function reachableAddresses(host, port, publicUrl = "") {
|
|
84
84
|
const link = (address) => {
|
|
85
85
|
// A bare IPv6 address needs brackets before it is a URL.
|
|
86
86
|
const authority = address.includes(":") ? `[${address}]` : address;
|
|
87
87
|
return `http://${authority}:${port}`;
|
|
88
88
|
};
|
|
89
|
+
// An address somebody told us about, because it is one this machine cannot
|
|
90
|
+
// know: a tunnel, a reverse proxy, or a router forwarding a port. It goes
|
|
91
|
+
// first so that it, and not a guess from an interface, is the address this
|
|
92
|
+
// stream is published under.
|
|
93
|
+
const told = publicUrl ? [{ label: "on the internet", url: publicUrl.replace(/\/+$/, "") }] : [];
|
|
89
94
|
if (host !== "0.0.0.0" && host !== "::")
|
|
90
|
-
return [{ label: "here", url: link(host) }];
|
|
95
|
+
return [...told, { label: "here", url: link(host) }];
|
|
91
96
|
const LABELS = { private: "on your network", cgnat: "on tailscale", public: "on the internet" };
|
|
92
97
|
const found = [];
|
|
93
98
|
for (const [name, entries] of Object.entries(networkInterfaces())) {
|
|
@@ -104,6 +109,7 @@ export function reachableAddresses(host, port) {
|
|
|
104
109
|
const order = { private: 0, cgnat: 1, public: 2 };
|
|
105
110
|
found.sort((x, y) => order[x.kind] - order[y.kind]);
|
|
106
111
|
return [
|
|
112
|
+
...told,
|
|
107
113
|
{ label: "here", url: `http://localhost:${port}` },
|
|
108
114
|
...found.map(({ label, url }) => ({ label, url })),
|
|
109
115
|
];
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -91,6 +91,9 @@ Options for serve:
|
|
|
91
91
|
--publish list it at nixamp.com/directory without asking first
|
|
92
92
|
--no-publish never list it, and do not ask
|
|
93
93
|
--name NAME what to call it in the directory (default: this hostname)
|
|
94
|
+
--public-url URL the address this server is reachable at from outside,
|
|
95
|
+
when that is a tunnel or a forwarded port rather than one of
|
|
96
|
+
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
94
97
|
--ingest accept a live stream in at POST /api/ingest
|
|
95
98
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
96
99
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
|
@@ -334,7 +337,10 @@ export async function main(): Promise<void> {
|
|
|
334
337
|
const asked = first ?? ".";
|
|
335
338
|
const target = isRemote(asked) ? asked : resolve(asked);
|
|
336
339
|
const tools = detectTools();
|
|
337
|
-
|
|
340
|
+
// Names now, tags later: an ffprobe per file over a large library is minutes
|
|
341
|
+
// of a blank terminal before the player appears. The list is the same list;
|
|
342
|
+
// only the titles arrive late, and they arrive into a player already running.
|
|
343
|
+
const tracks = await loadSource(tools, target, false);
|
|
338
344
|
if (tracks.length === 0) {
|
|
339
345
|
console.error(`nixamp: no audio files under ${target}`);
|
|
340
346
|
process.exit(1);
|
|
@@ -347,6 +353,21 @@ export async function main(): Promise<void> {
|
|
|
347
353
|
// closure stays narrowed to null for the checker.
|
|
348
354
|
const handoff: { to: { daemon: DaemonState; url: string } | null } = { to: null };
|
|
349
355
|
|
|
356
|
+
// The titles, arriving into a player that is already up. Not awaited, and
|
|
357
|
+
// applied only if the list is still the one it describes.
|
|
358
|
+
if (!isRemote(target)) {
|
|
359
|
+
void loadSource(tools, target, true)
|
|
360
|
+
.then((tagged) => {
|
|
361
|
+
if (tagged.length !== state.tracks.length) return;
|
|
362
|
+
if (tagged.some((track, at) => track.path !== state.tracks[at]?.path)) return;
|
|
363
|
+
state.tracks = tagged;
|
|
364
|
+
app.invalidate();
|
|
365
|
+
})
|
|
366
|
+
.catch(() => {
|
|
367
|
+
// Filenames play. Nothing to say about tags that would not read.
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
const analyser = new Analyser(FFT_SIZE, RATE);
|
|
351
372
|
const edges = bandEdges(BAND_COUNT, RATE, FFT_SIZE);
|
|
352
373
|
// Samples accumulate until there are enough for one transform.
|
package/src/server.ts
CHANGED
|
@@ -119,6 +119,15 @@ export interface ServeOptions {
|
|
|
119
119
|
publish: "ask" | "yes" | "no";
|
|
120
120
|
/** What to call it in the list. Defaults to this machine's hostname. */
|
|
121
121
|
name: string;
|
|
122
|
+
/**
|
|
123
|
+
* The address this server is reachable at from outside, when that is not one
|
|
124
|
+
* of its own interfaces: a tunnel, a reverse proxy, a forwarded port.
|
|
125
|
+
*
|
|
126
|
+
* Without it a machine behind NAT has nothing to publish -- every address it
|
|
127
|
+
* can see is a 192.168 one that is no use to anybody else -- so the directory
|
|
128
|
+
* listing is skipped and the printed links only work inside the house.
|
|
129
|
+
*/
|
|
130
|
+
publicUrl: string;
|
|
122
131
|
/**
|
|
123
132
|
* Charge for listening once the stream is busy. Off unless asked for, and
|
|
124
133
|
* useless without somewhere to pay: see NIXAMP_PAY_TO.
|
|
@@ -168,6 +177,7 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
168
177
|
directory: false,
|
|
169
178
|
publish: "ask",
|
|
170
179
|
name: "",
|
|
180
|
+
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
171
181
|
x402: false,
|
|
172
182
|
owner: "",
|
|
173
183
|
ingest: false,
|
|
@@ -208,6 +218,14 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
208
218
|
options.publish = "yes";
|
|
209
219
|
} else if (arg === "--no-publish") {
|
|
210
220
|
options.publish = "no";
|
|
221
|
+
} else if (arg === "--public-url") {
|
|
222
|
+
const given = value().trim();
|
|
223
|
+
// A hostname on its own is the likely typo, and it fails much later --
|
|
224
|
+
// as a directory listing nobody can open -- so it is refused here.
|
|
225
|
+
if (!/^https?:\/\/[^\s/]+/i.test(given)) {
|
|
226
|
+
throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
|
|
227
|
+
}
|
|
228
|
+
options.publicUrl = given.replace(/\/+$/, "");
|
|
211
229
|
} else if (arg === "--name") {
|
|
212
230
|
options.name = value();
|
|
213
231
|
} else if (arg === "--owner") {
|
|
@@ -343,6 +361,16 @@ export interface Engine {
|
|
|
343
361
|
* dropping every listener.
|
|
344
362
|
*/
|
|
345
363
|
replace(tracks: Track[], root: string): void;
|
|
364
|
+
/**
|
|
365
|
+
* The same tracks, now with their tags.
|
|
366
|
+
*
|
|
367
|
+
* Startup lists filenames and begins serving immediately, because an ffprobe
|
|
368
|
+
* per file over a real library takes minutes; the tags arrive afterwards and
|
|
369
|
+
* land here. Unlike `replace` this must not disturb anything -- whoever is
|
|
370
|
+
* listening keeps listening, and the only visible change is that the titles
|
|
371
|
+
* fill in.
|
|
372
|
+
*/
|
|
373
|
+
retag(tracks: Track[], root: string): void;
|
|
346
374
|
stop(): void;
|
|
347
375
|
}
|
|
348
376
|
|
|
@@ -542,6 +570,18 @@ export class PlayerEngine implements Engine {
|
|
|
542
570
|
this.state.note = "";
|
|
543
571
|
this.push();
|
|
544
572
|
}
|
|
573
|
+
|
|
574
|
+
retag(tracks: Track[], root: string): void {
|
|
575
|
+
// Dropped rather than applied if the library moved underneath: somebody
|
|
576
|
+
// re-streamed while the tagging was still running, and these tags describe
|
|
577
|
+
// something nobody is playing any more.
|
|
578
|
+
if (root !== this.root || tracks.length !== this.tracks.length) return;
|
|
579
|
+
if (tracks.some((track, at) => track.path !== this.tracks[at]?.path)) return;
|
|
580
|
+
this.tracks = tracks;
|
|
581
|
+
// No stop, no index reset: the only thing that changes is what the titles
|
|
582
|
+
// say, and every remote finds out because a snapshot goes out.
|
|
583
|
+
this.push();
|
|
584
|
+
}
|
|
545
585
|
}
|
|
546
586
|
|
|
547
587
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
@@ -559,6 +599,7 @@ export class EmptyEngine implements Engine {
|
|
|
559
599
|
return undefined;
|
|
560
600
|
}
|
|
561
601
|
replace(): void {}
|
|
602
|
+
retag(): void {}
|
|
562
603
|
stop(): void {}
|
|
563
604
|
}
|
|
564
605
|
|
|
@@ -2062,7 +2103,18 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2062
2103
|
const options = parseServeArgs(argv);
|
|
2063
2104
|
const root = isRemote(options.root) ? options.root : resolve(options.root);
|
|
2064
2105
|
const tools = detectTools();
|
|
2065
|
-
|
|
2106
|
+
// Names now, tags later.
|
|
2107
|
+
//
|
|
2108
|
+
// Reading tags is an ffprobe per file, which over a real library is minutes,
|
|
2109
|
+
// and every one of them used to happen before this process printed a word or
|
|
2110
|
+
// listened on a port. `nixamp serve ~/music` looked hung, and `nixamp daemon
|
|
2111
|
+
// start` was worse: it waits fifteen seconds for the announce line, killed a
|
|
2112
|
+
// daemon that was working perfectly, and reported a failure whose log was
|
|
2113
|
+
// empty because nothing had been written to it yet.
|
|
2114
|
+
//
|
|
2115
|
+
// So the filenames are enough to start: the server is up and answering in the
|
|
2116
|
+
// time it takes to walk the directory, and the titles fill in behind it.
|
|
2117
|
+
const tracks = await loadSource(tools, root, false);
|
|
2066
2118
|
const engine: Engine = tracks.length > 0
|
|
2067
2119
|
? new PlayerEngine(tracks, root, tools)
|
|
2068
2120
|
: new EmptyEngine(`No audio files under ${root}.`);
|
|
@@ -2342,12 +2394,25 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2342
2394
|
console.log(JSON.stringify({ nixamp: "listening", host: options.host, port, key, source: root }));
|
|
2343
2395
|
}
|
|
2344
2396
|
|
|
2397
|
+
// The other half of "names now, tags later". It runs while the banner is
|
|
2398
|
+
// printed and while the publish prompt waits, and it is deliberately not
|
|
2399
|
+
// awaited: nothing downstream needs it, and a library that takes a minute to
|
|
2400
|
+
// read should cost nobody a minute of silence.
|
|
2401
|
+
if (tracks.length > 0 && !isRemote(root)) {
|
|
2402
|
+
void loadSource(tools, root, true)
|
|
2403
|
+
.then((tagged) => engine.retag(tagged, root))
|
|
2404
|
+
.catch(() => {
|
|
2405
|
+
// Filenames are a working player. A failure here is worth nothing but
|
|
2406
|
+
// titles that stay as they are.
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2345
2410
|
console.log(`nixamp serve — ${tracks.length} tracks under ${root}`);
|
|
2346
2411
|
console.log("");
|
|
2347
2412
|
|
|
2348
2413
|
// The link, not the address. Without the key the address is a 401, so
|
|
2349
2414
|
// printing a bare host:port would be printing something that does not work.
|
|
2350
|
-
const addresses = reachableAddresses(options.host, port);
|
|
2415
|
+
const addresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2351
2416
|
const width = Math.max(...addresses.map((a) => a.label.length));
|
|
2352
2417
|
for (const { label, url } of addresses) {
|
|
2353
2418
|
console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
|
package/src/share.ts
CHANGED
|
@@ -93,14 +93,24 @@ export function classify(address: string): "private" | "cgnat" | "public" {
|
|
|
93
93
|
* somewhere else can open. It is labelled for what it is, because the key in
|
|
94
94
|
* the link is then the only thing between a stranger and the library.
|
|
95
95
|
*/
|
|
96
|
-
export function reachableAddresses(
|
|
96
|
+
export function reachableAddresses(
|
|
97
|
+
host: string,
|
|
98
|
+
port: number,
|
|
99
|
+
publicUrl = "",
|
|
100
|
+
): { label: string; url: string }[] {
|
|
97
101
|
const link = (address: string): string => {
|
|
98
102
|
// A bare IPv6 address needs brackets before it is a URL.
|
|
99
103
|
const authority = address.includes(":") ? `[${address}]` : address;
|
|
100
104
|
return `http://${authority}:${port}`;
|
|
101
105
|
};
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
// An address somebody told us about, because it is one this machine cannot
|
|
108
|
+
// know: a tunnel, a reverse proxy, or a router forwarding a port. It goes
|
|
109
|
+
// first so that it, and not a guess from an interface, is the address this
|
|
110
|
+
// stream is published under.
|
|
111
|
+
const told = publicUrl ? [{ label: "on the internet", url: publicUrl.replace(/\/+$/, "") }] : [];
|
|
112
|
+
|
|
113
|
+
if (host !== "0.0.0.0" && host !== "::") return [...told, { label: "here", url: link(host) }];
|
|
104
114
|
|
|
105
115
|
const LABELS = { private: "on your network", cgnat: "on tailscale", public: "on the internet" } as const;
|
|
106
116
|
const found: { label: string; url: string; kind: keyof typeof LABELS }[] = [];
|
|
@@ -116,6 +126,7 @@ export function reachableAddresses(host: string, port: number): { label: string;
|
|
|
116
126
|
const order = { private: 0, cgnat: 1, public: 2 } as const;
|
|
117
127
|
found.sort((x, y) => order[x.kind] - order[y.kind]);
|
|
118
128
|
return [
|
|
129
|
+
...told,
|
|
119
130
|
{ label: "here", url: `http://localhost:${port}` },
|
|
120
131
|
...found.map(({ label, url }) => ({ label, url })),
|
|
121
132
|
];
|
package/web/dist/sw.js
CHANGED