nixamp 0.5.10 → 0.5.12
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/dist/attach.js +5 -1
- package/dist/main.js +3 -0
- package/dist/protocol.d.ts +34 -2
- package/dist/protocol.js +11 -0
- package/dist/server.d.ts +31 -2
- package/dist/server.js +63 -16
- package/dist/share.d.ts +1 -1
- package/dist/share.js +3 -3
- package/package.json +1 -1
- package/src/attach.ts +5 -1
- package/src/main.ts +3 -0
- package/src/protocol.ts +37 -2
- package/src/server.ts +84 -17
- package/src/share.ts +3 -2
- package/web/dist/assets/{hls-3VKVEQE3-Cj5Lwh7A.js → hls-3VKVEQE3-Ch_vCVqk.js} +1 -1
- package/web/dist/assets/index-B816i-Dk.js +1 -0
- package/web/dist/assets/{mpegts-CKFUPaK9.js → mpegts-DS9rVyQe.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-Bj_ZfP5s.js → mpegts-LO6RVLD6-DxPAVtZQ.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-ABOb54bx.js +0 -1
package/src/server.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createReadStream, statSync } from "node:fs";
|
|
13
13
|
import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
14
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
14
15
|
import { hostname, networkInterfaces } from "node:os";
|
|
15
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
17
|
import { readFileSync } from "node:fs";
|
|
@@ -131,6 +132,18 @@ export interface ServeOptions {
|
|
|
131
132
|
* listing is skipped and the printed links only work inside the house.
|
|
132
133
|
*/
|
|
133
134
|
publicUrl: string;
|
|
135
|
+
/**
|
|
136
|
+
* A certificate and its key, to serve https rather than http.
|
|
137
|
+
*
|
|
138
|
+
* Needed by anybody whose nixamp is opened from a page that is itself https:
|
|
139
|
+
* a browser refuses every request from an https page to an http one --
|
|
140
|
+
* fetch, event stream and media alike -- and no header on either side lifts
|
|
141
|
+
* that. It is deliberately not required: a nixamp on 192.168.1.5 cannot have
|
|
142
|
+
* a certificate for that address, and forcing one would put a browser
|
|
143
|
+
* warning in front of everybody at home to fix a problem they do not have.
|
|
144
|
+
*/
|
|
145
|
+
tlsCert: string;
|
|
146
|
+
tlsKey: string;
|
|
134
147
|
/**
|
|
135
148
|
* Ask an outside service what this machine's public address is, when no
|
|
136
149
|
* interface holds one and none was given. Behind NAT that is the only way to
|
|
@@ -188,6 +201,8 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
188
201
|
name: "",
|
|
189
202
|
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
190
203
|
lookup: true,
|
|
204
|
+
tlsCert: process.env["NIXAMP_TLS_CERT"] ?? "",
|
|
205
|
+
tlsKey: process.env["NIXAMP_TLS_KEY"] ?? "",
|
|
191
206
|
x402: false,
|
|
192
207
|
owner: "",
|
|
193
208
|
ingest: false,
|
|
@@ -196,6 +211,11 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
196
211
|
rtmp: [],
|
|
197
212
|
};
|
|
198
213
|
let sawRoot = false;
|
|
214
|
+
const bothOrNeither = (): void => {
|
|
215
|
+
if (Boolean(options.tlsCert) !== Boolean(options.tlsKey)) {
|
|
216
|
+
throw new Error("nixamp serve: --tls-cert and --tls-key go together");
|
|
217
|
+
}
|
|
218
|
+
};
|
|
199
219
|
for (let i = 0; i < argv.length; i++) {
|
|
200
220
|
const arg = argv[i] as string;
|
|
201
221
|
const value = (): string => {
|
|
@@ -236,6 +256,10 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
236
256
|
throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
|
|
237
257
|
}
|
|
238
258
|
options.publicUrl = given.replace(/\/+$/, "");
|
|
259
|
+
} else if (arg === "--tls-cert") {
|
|
260
|
+
options.tlsCert = value();
|
|
261
|
+
} else if (arg === "--tls-key") {
|
|
262
|
+
options.tlsKey = value();
|
|
239
263
|
} else if (arg === "--no-lookup") {
|
|
240
264
|
options.lookup = false;
|
|
241
265
|
} else if (arg === "--name") {
|
|
@@ -271,6 +295,7 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
271
295
|
sawRoot = true;
|
|
272
296
|
}
|
|
273
297
|
}
|
|
298
|
+
bothOrNeither();
|
|
274
299
|
return options;
|
|
275
300
|
}
|
|
276
301
|
|
|
@@ -362,7 +387,8 @@ export function safeJoin(rootDir: string, urlPath: string): string | null {
|
|
|
362
387
|
|
|
363
388
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
364
389
|
export interface Engine {
|
|
365
|
-
|
|
390
|
+
/** `withTracks` false leaves the library out, for a frame that is only motion. */
|
|
391
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
366
392
|
command(command: Command): void;
|
|
367
393
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
368
394
|
/** Absolute path of a track, or undefined when the index is not one. */
|
|
@@ -476,10 +502,15 @@ export class PlayerEngine implements Engine {
|
|
|
476
502
|
this.dirty = true;
|
|
477
503
|
}
|
|
478
504
|
|
|
479
|
-
|
|
505
|
+
/**
|
|
506
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
507
|
+
* megabyte on a real one and is only news when it has changed.
|
|
508
|
+
*/
|
|
509
|
+
snapshot(withTracks = true): Snapshot {
|
|
480
510
|
return {
|
|
481
511
|
revision: this.revision,
|
|
482
|
-
tracks: toRemoteTracks(this.tracks),
|
|
512
|
+
...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
|
|
513
|
+
trackCount: this.tracks.length,
|
|
483
514
|
index: this.state.index,
|
|
484
515
|
playing: this.state.playing,
|
|
485
516
|
position: this.state.position,
|
|
@@ -569,10 +600,17 @@ export class PlayerEngine implements Engine {
|
|
|
569
600
|
};
|
|
570
601
|
}
|
|
571
602
|
|
|
572
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Send the state to everyone watching.
|
|
605
|
+
*
|
|
606
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
607
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
608
|
+
* to say about the track list, and it fires twelve times a second.
|
|
609
|
+
*/
|
|
610
|
+
private push(listChanged = false): void {
|
|
573
611
|
this.revision++;
|
|
574
612
|
if (this.listeners.size === 0) return;
|
|
575
|
-
const snapshot = this.snapshot();
|
|
613
|
+
const snapshot = this.snapshot(listChanged);
|
|
576
614
|
for (const listener of this.listeners) listener(snapshot);
|
|
577
615
|
}
|
|
578
616
|
|
|
@@ -592,7 +630,7 @@ export class PlayerEngine implements Engine {
|
|
|
592
630
|
this.state.index = 0;
|
|
593
631
|
this.state.position = 0;
|
|
594
632
|
this.state.note = "";
|
|
595
|
-
this.push();
|
|
633
|
+
this.push(true);
|
|
596
634
|
}
|
|
597
635
|
|
|
598
636
|
retag(tracks: Track[], root: string): void {
|
|
@@ -603,8 +641,9 @@ export class PlayerEngine implements Engine {
|
|
|
603
641
|
if (tracks.some((track, at) => track.path !== this.tracks[at]?.path)) return;
|
|
604
642
|
this.tracks = tracks;
|
|
605
643
|
// No stop, no index reset: the only thing that changes is what the titles
|
|
606
|
-
// say, and every remote finds out because a snapshot goes out
|
|
607
|
-
this.
|
|
644
|
+
// say, and every remote finds out because a snapshot goes out -- carrying
|
|
645
|
+
// the list, since the titles are the whole point of this one.
|
|
646
|
+
this.push(true);
|
|
608
647
|
}
|
|
609
648
|
}
|
|
610
649
|
|
|
@@ -614,6 +653,7 @@ export class EmptyEngine implements Engine {
|
|
|
614
653
|
snapshot(): Snapshot {
|
|
615
654
|
return { ...emptySnapshot(), note: this.note };
|
|
616
655
|
}
|
|
656
|
+
|
|
617
657
|
command(): void {}
|
|
618
658
|
subscribe(listener: (snapshot: Snapshot) => void): () => void {
|
|
619
659
|
listener(this.snapshot());
|
|
@@ -724,6 +764,8 @@ export interface HandlerOptions {
|
|
|
724
764
|
signIn?: SignIn;
|
|
725
765
|
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
726
766
|
secureCookies?: boolean;
|
|
767
|
+
/** A certificate and key in PEM, when this server is to speak https itself. */
|
|
768
|
+
tls?: { cert: string; key: string };
|
|
727
769
|
/**
|
|
728
770
|
* True when a proxy sits in front, so `x-forwarded-for` names the caller.
|
|
729
771
|
* False everywhere else on purpose: the header is trivially forged, and
|
|
@@ -1811,7 +1853,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1811
1853
|
json(response, 404, { error: "no such track" });
|
|
1812
1854
|
return;
|
|
1813
1855
|
}
|
|
1814
|
-
watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
|
|
1856
|
+
watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
|
|
1815
1857
|
// A browser asks for every track here, and a matroska or an avi handed
|
|
1816
1858
|
// to it raw is bytes it cannot play. Seeking is what this route is for
|
|
1817
1859
|
// and transcoding gives it up, but an unseekable film beats a silent
|
|
@@ -1842,11 +1884,11 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1842
1884
|
return;
|
|
1843
1885
|
}
|
|
1844
1886
|
const current = engine.snapshot();
|
|
1845
|
-
if (current.
|
|
1887
|
+
if (current.trackCount === 0) {
|
|
1846
1888
|
json(response, 404, { error: "nothing is playing" });
|
|
1847
1889
|
return;
|
|
1848
1890
|
}
|
|
1849
|
-
watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
|
|
1891
|
+
watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
|
|
1850
1892
|
liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
|
|
1851
1893
|
return;
|
|
1852
1894
|
}
|
|
@@ -1862,7 +1904,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1862
1904
|
json(response, 403, { error: "media streaming is off" });
|
|
1863
1905
|
return;
|
|
1864
1906
|
}
|
|
1865
|
-
watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
|
|
1907
|
+
watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
|
|
1866
1908
|
transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
|
|
1867
1909
|
return;
|
|
1868
1910
|
}
|
|
@@ -2186,12 +2228,19 @@ function sendFile(request: IncomingMessage, response: ServerResponse, file: stri
|
|
|
2186
2228
|
|
|
2187
2229
|
export function createServer(engine: Engine, options: HandlerOptions): Server {
|
|
2188
2230
|
const handle = createHandler(engine, options);
|
|
2189
|
-
|
|
2231
|
+
const onRequest = (request: IncomingMessage, response: ServerResponse): void => {
|
|
2190
2232
|
handle(request, response).catch(() => {
|
|
2191
2233
|
if (!response.headersSent) json(response, 500, { error: "server error" });
|
|
2192
2234
|
else response.end();
|
|
2193
2235
|
});
|
|
2194
|
-
}
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
// https when there is a certificate to serve it with, and the same handler
|
|
2239
|
+
// either way: nothing above this line knows or cares which it got.
|
|
2240
|
+
if (options.tls) {
|
|
2241
|
+
return createHttpsServer({ cert: options.tls.cert, key: options.tls.key }, onRequest) as unknown as Server;
|
|
2242
|
+
}
|
|
2243
|
+
return createHttpServer(onRequest);
|
|
2195
2244
|
}
|
|
2196
2245
|
|
|
2197
2246
|
|
|
@@ -2419,6 +2468,18 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2419
2468
|
});
|
|
2420
2469
|
}
|
|
2421
2470
|
|
|
2471
|
+
// Read before listening, so a missing or unreadable certificate is a sentence
|
|
2472
|
+
// now rather than a connection that resets later.
|
|
2473
|
+
const tls = options.tlsCert
|
|
2474
|
+
? (() => {
|
|
2475
|
+
try {
|
|
2476
|
+
return { cert: readFileSync(options.tlsCert, "utf8"), key: readFileSync(options.tlsKey, "utf8") };
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
throw new Error(`nixamp serve: could not read the certificate: ${(error as Error).message}`);
|
|
2479
|
+
}
|
|
2480
|
+
})()
|
|
2481
|
+
: undefined;
|
|
2482
|
+
|
|
2422
2483
|
const server = createServer(engine, {
|
|
2423
2484
|
web,
|
|
2424
2485
|
media: options.media,
|
|
@@ -2434,6 +2495,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2434
2495
|
paywall,
|
|
2435
2496
|
ffmpeg: tools.ffmpeg,
|
|
2436
2497
|
ffprobe: tools.ffprobe,
|
|
2498
|
+
...(tls ? { tls } : {}),
|
|
2437
2499
|
load: (next) => loadSource(tools, next),
|
|
2438
2500
|
...(directory ? { directory } : {}),
|
|
2439
2501
|
...(follows ? { follows, vapidPublicKey } : {}),
|
|
@@ -2499,13 +2561,18 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2499
2561
|
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2500
2562
|
// router and not about this port -- the port still has to be forwarded -- so
|
|
2501
2563
|
// it is marked as a guess and everything that prints it says so.
|
|
2502
|
-
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2564
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl, tls ? "https" : "http");
|
|
2503
2565
|
const guessedPublic =
|
|
2504
2566
|
options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2505
2567
|
? await lookupPublicIp()
|
|
2506
2568
|
: "";
|
|
2507
2569
|
const addresses = guessedPublic
|
|
2508
|
-
? reachableAddresses(
|
|
2570
|
+
? reachableAddresses(
|
|
2571
|
+
options.host,
|
|
2572
|
+
port,
|
|
2573
|
+
`${tls ? "https" : "http"}://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`,
|
|
2574
|
+
tls ? "https" : "http",
|
|
2575
|
+
)
|
|
2509
2576
|
: localAddresses;
|
|
2510
2577
|
|
|
2511
2578
|
// Listening on every interface proves the socket is open here and nothing
|
|
@@ -2681,7 +2748,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2681
2748
|
},
|
|
2682
2749
|
nowPlaying: () => {
|
|
2683
2750
|
const snapshot = engine.snapshot();
|
|
2684
|
-
return snapshot.tracks[snapshot.index]?.title ?? "";
|
|
2751
|
+
return snapshot.tracks?.[snapshot.index]?.title ?? "";
|
|
2685
2752
|
},
|
|
2686
2753
|
onConfig: (remote) => {
|
|
2687
2754
|
const next = applyRemoteConfig(paywallConfig, (remote as { x402?: unknown })?.x402);
|
package/src/share.ts
CHANGED
|
@@ -132,11 +132,12 @@ export function reachableAddresses(
|
|
|
132
132
|
host: string,
|
|
133
133
|
port: number,
|
|
134
134
|
publicUrl = "",
|
|
135
|
+
scheme: "http" | "https" = "http",
|
|
135
136
|
): { label: string; url: string }[] {
|
|
136
137
|
const link = (address: string): string => {
|
|
137
138
|
// A bare IPv6 address needs brackets before it is a URL.
|
|
138
139
|
const authority = address.includes(":") ? `[${address}]` : address;
|
|
139
|
-
return
|
|
140
|
+
return `${scheme}://${authority}:${port}`;
|
|
140
141
|
};
|
|
141
142
|
|
|
142
143
|
// An address somebody told us about, because it is one this machine cannot
|
|
@@ -162,7 +163,7 @@ export function reachableAddresses(
|
|
|
162
163
|
found.sort((x, y) => order[x.kind] - order[y.kind]);
|
|
163
164
|
return [
|
|
164
165
|
...told,
|
|
165
|
-
{ label: "here", url:
|
|
166
|
+
{ label: "here", url: `${scheme}://localhost:${port}` },
|
|
166
167
|
...found.map(({ label, url }) => ({ label, url })),
|
|
167
168
|
];
|
|
168
169
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-B816i-Dk.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){if(!Number.isFinite(e)||e<0)return`--:--`;let t=Math.floor(e),n=Math.floor(t/60),r=t%60;return`${String(n).padStart(2,`0`)}:${String(r).padStart(2,`0`)}`}function t(e){return e.artist?`${e.artist} — ${e.title}`:e.title}function n(e){let t=e.split(`/`).pop()??e,n=t.lastIndexOf(`.`);return n>0?t.slice(0,n):t}var r=new Set([`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`,`avi`]);function i(e,t=``){if(t.startsWith(`video/`))return!0;if(t.startsWith(`audio/`))return!1;let n=e.lastIndexOf(`.`);return n>0&&r.has(e.slice(n+1).toLowerCase())}var a=`modulepreload`,o=function(e){return`/`+e},s={},c=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),c=i?.nonce||i?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function u(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=l(t.map(t=>{if(t=o(t,n),t=u(t),t in s)return;s[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:a,r||(i.as=`script`),i.crossOrigin=``,i.href=t,c&&i.setAttribute(`nonce`,c),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},l=[[/\.m3u8$/i,`hls`],[/\.(ts|mts|m2ts|mpegts)$/i,`mpegts`],[/\.(mp3|m4a|aac|oga|ogg|opus|wav|flac)$/i,`audio`],[/\.(mp4|m4v|webm|mov|ogv)$/i,`mp4`]],u=[[/mpegurl/i,`hls`],[/mp2t|mpeg-?ts/i,`mpegts`],[/^audio\//i,`audio`],[/^video\//i,`mp4`]];function d(e){if(e.kind)return e.kind;if(e.mimeType){for(let[t,n]of u)if(t.test(e.mimeType))return n}let t=f(e.src);for(let[e,n]of l)if(e.test(t))return n;return`unknown`}function f(e){try{return new URL(e,`https://placeholder.invalid`).pathname}catch{return e.split(/[?#]/)[0]??e}}function p(e=globalThis,t=null){let n=e.MediaSource!==void 0,r=!1;try{let e=t??(typeof document>`u`?null:document.createElement(`video`));r=e?e.canPlayType(`application/vnd.apple.mpegurl`)!==``||e.canPlayType(`application/x-mpegURL`)!==``:!1}catch{r=!1}return{mediaSource:n,nativeHls:r}}function m(e,t){let n=d(e);switch(n){case`hls`:return t.mediaSource?{engine:`hls`,kind:n}:t.nativeHls?{engine:`native`,kind:n}:{engine:`native`,kind:n,unplayable:`This browser cannot play HLS streams.`};case`mpegts`:return t.mediaSource?{engine:`mpegts`,kind:n}:{engine:`mpegts`,kind:n,unplayable:`This browser cannot play transport streams.`};default:return{engine:`native`,kind:n}}}async function h(e,t){let n=t.capabilities??p(),r=m({src:t.src,...t.kind?{kind:t.kind}:{},...t.mimeType?{mimeType:t.mimeType}:{}},n),i=()=>void 0,a={media:e,src:t.src,isTv:t.isTv??!1,live:t.live??r.kind===`mpegts`,onError:t.onError??i,onNotice:t.onNotice??i,...t.onReady?{onReady:t.onReady}:{}};if(r.unplayable)return t.onError?.(r.unplayable),{destroy:i,engine:r.engine,kind:r.kind,levels:()=>[],unplayable:r.unplayable};let o,s=t.engines?.[r.engine];if(s)o=await s(a);else if(r.engine===`hls`){let{createHlsEngine:e}=await c(async()=>{let{createHlsEngine:e}=await import(`./hls-3VKVEQE3-Ch_vCVqk.js`);return{createHlsEngine:e}},[]);o=await e(a)}else if(r.engine===`mpegts`){let{createMpegtsEngine:e}=await c(async()=>{let{createMpegtsEngine:e}=await import(`./mpegts-LO6RVLD6-DxPAVtZQ.js`);return{createMpegtsEngine:e}},[]);o=await e(a,{withCredentials:t.withCredentials??!1,unplayableAdvice:t.unplayableAdvice??``})}else{let{createNativeEngine:e}=await c(async()=>{let{createNativeEngine:e}=await import(`./native-C7JTKWJH-BUyIoj0P.js`);return{createNativeEngine:e}},[]);o=await e(a)}return{destroy:()=>{o.destroy()},engine:r.engine,kind:r.kind,levels:o.levels,...o.setLevel?{setLevel:o.setLevel}:{},...o.currentLevel?{currentLevel:o.currentLevel}:{}}}var g=new Set([`mp3`,`flac`,`ogg`,`oga`,`opus`,`m4a`,`aac`,`wav`,`wma`,`aiff`,`aif`,`alac`,`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`]);function _(e,t=``){if(t.startsWith(`audio/`)||t.startsWith(`video/`))return!0;let n=e.lastIndexOf(`.`);return n>0&&g.has(e.slice(n+1).toLowerCase())}function v(e,t){return e.localeCompare(t,void 0,{numeric:!0,sensitivity:`base`})}function y(e){return e.filter(e=>_(e.name,e.type)).sort((e,t)=>v(b(e),b(t))).map(e=>({title:n(e.name),artist:``,album:x(b(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function b(e){return e.webkitRelativePath||e.name}function x(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function ee(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var S=2048;function C(e){return e===`audio`}var te=class{elements;handlers;attached=null;context=null;analyser=null;wired=new WeakSet;active;frequencies=new Uint8Array;constructor(e,t){this.elements=e,this.handlers=t,this.active=e.audio;for(let t of[e.audio,e.video])t.crossOrigin=`anonymous`,t.addEventListener(`timeupdate`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`loadedmetadata`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`ended`,()=>{t===this.active&&this.handlers.onEnded()}),t.addEventListener(`play`,()=>{t===this.active&&this.handlers.onState(!0)}),t.addEventListener(`pause`,()=>{t===this.active&&this.handlers.onState(!1)}),t.addEventListener(`error`,()=>{t===this.active&&this.handlers.onError(w(t))})}get playing(){return!this.active.paused&&!this.active.ended}get position(){return this.active.currentTime}get duration(){return Number.isFinite(this.active.duration)?this.active.duration:0}get showingVideo(){return this.active===this.elements.video}ensureGraph(e){let t=globalThis.AudioContext??globalThis.webkitAudioContext;if(t){if(this.context??=new t,this.analyser||(this.analyser=this.context.createAnalyser(),this.analyser.fftSize=S,this.analyser.smoothingTimeConstant=.6,this.analyser.connect(this.context.destination),this.frequencies=new Uint8Array(this.analyser.frequencyBinCount)),!this.wired.has(e))try{this.context.createMediaElementSource(e).connect(this.analyser),this.wired.add(e)}catch{this.wired.add(e)}this.context.resume()}}read(){return this.analyser&&this.analyser.getByteFrequencyData(this.frequencies),this.frequencies}levels(){if(!this.analyser)return[0,0];let e=this.read(),t=0;for(let n of e)t+=n;let n=e.length===0?0:t/e.length/255;return[Math.min(1,n*2.2),Math.min(1,n*2.2)]}async load(e,t){let n=e.objectUrl?e.video?`mp4`:`audio`:d({src:e.url}),r=e.video||!C(n)?this.elements.video:this.elements.audio;r!==this.active&&(this.active.pause(),this.active.removeAttribute(`src`),this.active.load(),this.active=r),this.attached?.destroy(),this.attached=null;try{this.attached=await h(this.active,{src:e.url,kind:n,unplayableAdvice:`VLC or mpv will play it; nixamp can only hand it to your browser.`,onError:e=>this.handlers.onError(e),onNotice:e=>{e&&this.handlers.onError(e)}})}catch(e){this.handlers.onError(e instanceof Error?e.message:`that would not play`);return}t&&await this.play()}async play(){this.ensureGraph(this.active);try{await this.active.play()}catch(e){this.handlers.onError(e instanceof Error?e.message:`playback was refused`)}}pause(){this.active.pause()}stop(){this.active.pause(),this.active.currentTime=0,this.attached?.destroy(),this.attached=null}seek(e){Number.isFinite(e)&&(this.active.currentTime=Math.max(0,e))}set volume(e){this.elements.audio.volume=e,this.elements.video.volume=e}get volume(){return this.active.volume}};function w(e){switch(e.error?.code){case MediaError.MEDIA_ERR_ABORTED:return`playback was aborted`;case MediaError.MEDIA_ERR_NETWORK:return`the network dropped mid-track`;case MediaError.MEDIA_ERR_DECODE:return`this browser could not decode that`;case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:return`this browser cannot play that format`;default:return`playback failed`}}function T(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function ne(e,t){return{...t,tracks:t.tracks??e.tracks}}function E(e){let t=e.trim();if(t===``)return``;/^https?:\/\//i.test(t)||(t=`http://${t}`);let n;try{n=new URL(t)}catch{return``}let r=n.pathname.replace(/\/+$/,``);return r=r.replace(/\/api(\/.*)?$/,``),`${n.origin}${r}`}function D(e,t){return`${e===``?``:E(e)}${t.startsWith(`/`)?t:`/${t}`}`}function O(e,t){return D(e,`/api/media/${t}`)}function k(e){if(typeof e!=`object`||!e)return null;let t=e,n=T(),r=(e,t)=>typeof e==`number`&&Number.isFinite(e)?e:t,i=Array.isArray(t.levels)?t.levels:[],a=Array.isArray(t.tracks)?t.tracks.map(e=>{let t=typeof e==`object`&&e?e:{};return{title:typeof t.title==`string`?t.title:`Untitled`,artist:typeof t.artist==`string`?t.artist:``,album:typeof t.album==`string`?t.album:``,duration:r(t.duration,0),...t.video===!0?{video:!0}:{}}}):void 0;return{revision:r(t.revision,0),...a?{tracks:a}:{},trackCount:r(t.trackCount,a?.length??0),index:r(t.index,0),playing:t.playing===!0,position:r(t.position,0),bars:Array.isArray(t.bars)?t.bars.map(e=>r(e,0)):[],levels:[r(i[0],0),r(i[1],0)],silent:t.silent===!0,note:typeof t.note==`string`?t.note:``,root:typeof t.root==`string`?t.root:n.root}}var re=class{handlers;source=null;base=``;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}get connected(){return this.source!==null}connect(e){let t=E(e);this.close(),this.base=t,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let n=new EventSource(D(t,`/api/events`));this.source=n,n.onopen=()=>this.handlers.onStatus(`live`),n.onmessage=e=>{let t=k(A(e.data));t&&(t.revision<this.lastRevision||(this.lastRevision=t.revision,this.handlers.onStatus(`live`),this.handlers.onSnapshot(t)))},n.onerror=()=>{this.handlers.onStatus(`error`,`reconnecting…`)}}async send(e){if(this.base===``&&!this.connected)return;let t=await fetch(D(this.base,`/api/command`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(e)});if(!t.ok){this.handlers.onStatus(`error`,`command refused (${t.status})`);return}let n=k(await t.json());n&&this.handlers.onSnapshot(n)}media(e){return O(this.base,e)}close(){this.source?.close(),this.source=null}};function A(e){try{return JSON.parse(e)}catch{return null}}async function ie(e,t){try{let n=await fetch(D(e,`/api/state`),{signal:t});return n.ok?k(await n.json()):null}catch{return null}}async function ae(e,t){try{let n=await fetch(D(e,`/api/health`),{signal:t});if(!n.ok)return null;let r=await n.json();return r.name===`nixamp`?r.version??`unknown`:null}catch{return null}}function oe(e,t=globalThis.location?.protocol){return t!==`https:`||!/^http:\/\//i.test(e.trim())?``:`This page is https, and a browser refuses every request from an https page to an http one. Open that address directly, or give the server a certificate: nixamp serve --tls-cert cert.pem --tls-key key.pem.`}var j=.14,M=.02;function se(e,t){let n=[];for(let r=0;r<=e;r++){let i=r/e,a=Math.round(1*(t/1)**i),o=n[n.length-1];n.push(o===void 0?a:Math.max(a,o+1))}return n}function ce(e,t){let n=[];for(let r=0;r+1<t.length;r++){let i=Math.min(t[r],e.length),a=Math.min(Math.max(t[r+1],i+1),e.length),o=0,s=0;for(let t=i;t<a;t++)o+=e[t],s++;n.push(s===0?0:o/s/255)}return n}function le(e,t,n=j){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function N(e,t,n=M){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function ue(e,t,n,r,i){let{width:a,height:o}=t;if(a<=0||o<=0||n.length===0)return;e.clearRect(0,0,a,o);let s=a/n.length,c=Math.max(1,s*.72),l=Math.max(2,o*.012);e.fillStyle=i.bar,n.forEach((t,n)=>{let r=Math.max(1,t*(o-l*2));e.fillRect(n*s+(s-c)/2,o-r,c,r)}),e.fillStyle=i.peak,r.forEach((t,n)=>{let r=o-Math.max(1,t*(o-l*2))-l*2;e.fillRect(n*s+(s-c)/2,Math.max(0,r),c,l)})}var P=`nixamp.remote`,F=`nixamp.volume`;function I(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function L(){let n={status:I(`status`),source:I(`source`),install:I(`install`),video:I(`video`),audio:I(`audio`),title:I(`title-line`),album:I(`album-line`),elapsed:I(`elapsed`),total:I(`total`),seek:I(`seek`),canvas:I(`spectrum`),glyphs:I(`glyphs`),levels:I(`levels`),playlist:I(`playlist`),playlistTitle:I(`playlist-panel`),note:I(`note`),files:I(`files`),folder:I(`folder`),remoteUrl:I(`remote-url`),remoteForm:I(`remote-form`),remoteState:I(`remote-state`),disconnect:I(`disconnect`),browse:I(`browse`),accountForm:I(`account-form`),accountEmail:I(`account-email`),accountPassword:I(`account-password`),accountSubmit:I(`account-submit`),accountToggle:I(`account-toggle`),accountProviders:I(`account-providers`),accountPanel:I(`account-panel`),accountElsewhere:I(`account-elsewhere`),accountSignOut:I(`account-signout`),accountNote:I(`account-note`),adminPanel:I(`admin-panel`),adminNote:I(`admin-note`),adminConnections:I(`admin-connections`),adminRestream:I(`admin-restream`),adminSource:I(`admin-source`),directory:I(`directory`),recentNote:I(`recent-note`),recentList:I(`recent-list`),followingNote:I(`following-note`),followingList:I(`following-list`),notifyPanel:I(`notify-panel`),notifyNote:I(`notify-note`),notifyWeb:I(`notify-web`),notifyEmail:I(`notify-email`),notifySms:I(`notify-sms`),notifyPhone:I(`notify-phone`),notifyPhoneForm:I(`notify-phone-form`),notifyPhoneNote:I(`notify-phone-note`),directoryNote:I(`directory-note`),directoryList:I(`directory-list`),listenHere:I(`listen-here`),volume:I(`volume`),prev:I(`prev`),playPause:I(`play-pause`),stop:I(`stop`),next:I(`next`)},r=`local`,i=[],a=0,o=T(),s=`idle`,c=``,l=`Pick files, or connect to a nixamp running somewhere else.`,u=!1,d=Array(24).fill(0),f=Array(24).fill(0),p=[],m=()=>r===`remote`&&!n.listenHere.checked,h=new te({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),L()},onEnded:()=>A(1),onState:()=>L(),onError:e=>{l=e,L()}}),g=new re({onSnapshot:e=>{o=ne(o,e),m()&&(d=e.bars.length>0?e.bars:d,f=N(f,d)),L()},onStatus:(e,t)=>{s=e,c=t??``,L()}}),_=()=>r===`remote`?o.tracks.length:i.length,v=()=>r===`remote`?o.index:a,b=()=>{if(r===`remote`){let e=o.tracks[o.index];return e?t(e):`Nothing loaded.`}let e=i[a];return e?t(e):`Nothing loaded.`},x=()=>(r===`remote`?o.tracks[o.index]:i[a])?.album||`—`,S=()=>m()?o.tracks[o.index]?.duration??0:h.duration,C=()=>m()?o.position:h.position,w=()=>m()?o.playing:h.playing;async function D(e){if(r===`remote`){if(m()){await g.send({type:`play`,index:e});return}await g.send({type:`select`,index:e}),await O(e);return}let t=i[e];t&&(a=e,await h.load(t,!0),B(t.video),V(),L())}async function O(e){let t=o.tracks[e];t&&(await h.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:g.media(e),video:t.video===!0,objectUrl:!1},!0),B(t.video===!0),V())}async function k(){if(m()){await g.send({type:`toggle`});return}_()!==0&&(h.playing?h.pause():h.position>0?await h.play():await D(v()),L())}async function A(e){let t=_();if(t!==0){if(m()){await g.send({type:e>0?`next`:`prev`});return}await D((v()+e+t)%t)}}async function j(){if(m()){await g.send({type:`stop`});return}h.stop(),d=Array(24).fill(0),f=[...d],L()}let M=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function L(){let t=_(),a=w();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=b(),n.album.textContent=x();let f=C(),p=S();n.elapsed.textContent=e(f),n.total.textContent=p>0?e(p):`--:--`,u||(n.seek.value=String(p>0?Math.round(f/p*1e3):0),n.seek.disabled=p<=0||m()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${g.address.replace(/^https?:\/\//,``)||`—`}`:i.length>0?`local · ${i.length} files`:`no source`,n.remoteState.textContent=r===`remote`?`${s}${c?` — ${c}`:``}`:`not connected`,n.remoteState.dataset.status=r===`remote`?s:`idle`,n.disconnect.hidden=r!==`remote`;let v=r===`remote`&&o.note!==``?o.note:l;n.note.textContent=v,n.note.hidden=v===``,de(),n.glyphs.textContent=d.map(M).join(``);let[y,ee]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(y*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)}`}let R=``;function de(){let a=r===`remote`?o.tracks.map(e=>[t(e),e.duration]):i.map(e=>[t(e),e.duration]),s=`${r}:${a.map(([e,t])=>`${e}@${t}`).join(`|`)}`;s!==R&&(R=s,n.playlist.replaceChildren(...a.map(([t,n],r)=>{let i=document.createElement(`li`);i.className=`row`,i.dataset.index=String(r);let a=document.createElement(`span`);a.className=`n`,a.textContent=String(r+1).padStart(2,` `);let o=document.createElement(`span`);o.className=`name`,o.textContent=t;let s=document.createElement(`span`);return s.className=`time`,s.textContent=n>0?e(n):`--:--`,i.append(a,o,s),i})));let c=v(),l=w();Array.from(n.playlist.children).forEach((e,t)=>{let n=e;n.classList.toggle(`selected`,t===c),n.classList.toggle(`playing`,t===c&&l)}),n.playlist.children[c]?.scrollIntoView({block:`nearest`})}function z(){let t=n.canvas,r=Math.min(2,globalThis.devicePixelRatio||1),i=Math.round(t.clientWidth*r),a=Math.round(t.clientHeight*r);i>0&&a>0&&(t.width!==i||t.height!==a)&&(t.width=i,t.height=a);let s=t.getContext(`2d`);if(m())f=N(f,d);else{let e=h.read();e.length>0&&(p.length!==25&&(p=se(24,e.length)),d=le(d,ce(e,p)),f=N(f,d))}if(s){let e=getComputedStyle(document.documentElement);ue(s,{width:t.width,height:t.height},d,f,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(w()){n.glyphs.textContent=d.map(M).join(``);let[t,r]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(C());let i=S();!u&&i>0&&(n.seek.value=String(Math.round(C()/i*1e3)))}requestAnimationFrame(z)}function B(e){n.video.hidden=!e}function V(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:b(),album:x(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void k()),navigator.mediaSession.setActionHandler(`pause`,()=>void k()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void A(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void A(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&D(n)}),n.prev.addEventListener(`click`,()=>void A(-1)),n.next.addEventListener(`click`,()=>void A(1)),n.stop.addEventListener(`click`,()=>void j()),n.playPause.addEventListener(`click`,()=>void k()),n.seek.addEventListener(`input`,()=>{u=!0}),n.seek.addEventListener(`change`,()=>{let e=S();e>0&&h.seek(Number(n.seek.value)/1e3*e),u=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;h.volume=e;try{localStorage.setItem(F,String(e))}catch{}});let H=e=>{e.addEventListener(`change`,()=>{let t=y(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,L();return}ee(i),i=t,a=0,r=`local`,g.close(),l=``,D(0)})};H(n.files),H(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=E(n.remoteUrl.value);if(t===``){l=`That is not an address.`,L();return}(async()=>{s=`connecting`,L();let e=oe(t);if(e){s=`error`,c=e,l=e,r=`local`,L();return}if(await ae(t)===null){s=`error`,c=`no nixamp answered there`,r=`local`,L();return}r=`remote`,l=``;try{localStorage.setItem(P,t)}catch{}g.connect(t),L()})()});let U=async()=>{n.directory.hidden=!1,n.directoryNote.textContent=`Looking for live streams…`,n.directoryList.replaceChildren();let e;try{let t=await fetch(`/api/directory`);if(!t.ok)throw Error(String(t.status));let n=await t.json();e=n.streams??[],pe(n.recent??[])}catch{n.directoryNote.textContent=`The directory is not answering. Type an address instead.`;return}if(e.length===0){n.directoryNote.textContent=`Nobody is streaming right now.`;return}n.directoryNote.textContent=`${e.length} live ${e.length===1?`stream`:`streams`}:`;for(let t of e){let e=document.createElement(`li`),r=document.createElement(`button`);r.type=`button`;let i=document.createElement(`span`);i.className=`name`,i.textContent=t.name;let a=document.createElement(`span`);a.className=`detail`;let o=[t.nowPlaying,`${t.tracks} tracks`].filter(Boolean);t.code&&o.push(t.callers?`☎ ${t.code} · ${t.callers} on the phone`:`☎ ${t.code}`),a.textContent=o.join(` · `),r.append(i,a),r.addEventListener(`click`,()=>{n.remoteUrl.value=t.url,n.directory.hidden=!0,n.remoteForm.requestSubmit()}),e.append(r),t.ownerId&&Z&&t.ownerId!==Z&&e.append(J(t.ownerId,t.name)),n.directoryList.append(e)}};if(location.pathname.replace(/\/+$/,``)===`/directory`){document.body.classList.add(`route-directory`);let e=document.getElementById(`directory-back`);e&&(e.hidden=!1),U()}let W=null,fe=e=>{n.adminConnections.replaceChildren();let t=document.createElement(`tr`);for(let e of[`Where`,`Network`,`Kind`,`Client`,`Track`,`Sent`]){let n=document.createElement(`th`);n.textContent=e,t.append(n)}n.adminConnections.append(t);for(let t of e.slice(0,40)){let e=document.createElement(`tr`);t.endedAt!==null&&(e.className=`ended`);let r=[[t.address,``],[t.network,`network-${t.network}`],[t.kind,``],[t.agent,``],[t.track||`—`,``],[`${Math.round(t.bytes/1024)} KiB`,``]];for(let[t,n]of r){let r=document.createElement(`td`);r.textContent=t,n&&(r.className=n),e.append(r)}n.adminConnections.append(e)}},G=async()=>{try{let e=await fetch(`/api/connections`);if(!e.ok)return;let t=await e.json();n.adminNote.textContent=`${t.active??0} listening now.`,fe(t.connections??[])}catch{n.adminNote.textContent=`lost touch with the server`}},K=async()=>{let e=!1,t=null;try{let n=await fetch(`/api/admin`);if(n.ok){let r=await n.json();e=r.allowed===!0,t=r.as??null}}catch{e=!1}n.adminPanel.hidden=!e,W&&clearInterval(W),W=null,e&&(n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,G(),W=setInterval(()=>void G(),2e3))};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();t&&(async()=>{try{let e=await fetch(`/api/source`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t})}),r=await e.json();n.adminNote.textContent=e.ok?`Now serving ${t}.`:r.error??`that did not work`,e.ok&&(n.adminSource.value=``)}catch{n.adminNote.textContent=`could not reach the server`}})()});let q=e=>{let t=Math.max(1,Math.round((Date.now()-e)/6e4));if(t<60)return`${t} minute${t===1?``:`s`} ago`;let n=Math.round(t/60);return`${n} hour${n===1?``:`s`} ago`},pe=e=>{n.recentList.replaceChildren();let t=Z?e.filter(e=>e.ownerId&&e.ownerId!==Z):[];if(n.recentNote.hidden=t.length===0,t.length!==0)for(let e of t){let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`recent-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.nowPlaying?`${e.nowPlaying} · ended ${q(e.endedAt)}`:`ended ${q(e.endedAt)}`,r.append(i,a),t.append(r,J(e.ownerId,e.name)),n.recentList.append(t)}},me=async()=>{n.followingList.replaceChildren();try{let e=await fetch(`/api/v1/follows`);if(!e.ok){n.followingNote.hidden=!0;return}let t=(await e.json()).following??[];n.followingNote.hidden=t.length===0;for(let e of t){let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`recent-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name||`a nixamp`;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.live?`live now`:`not streaming`,r.append(i,a);let o=document.createElement(`button`);o.type=`button`,o.className=`ghost follow`,o.textContent=`Unfollow`,o.addEventListener(`click`,()=>{(async()=>{o.disabled=!0;try{await fetch(`/api/v1/follows/${encodeURIComponent(e.id)}`,{method:`DELETE`}),t.remove(),n.followingList.children.length===0&&(n.followingNote.hidden=!0)}finally{o.disabled=!1}})()}),t.append(r,o),n.followingList.append(t)}}catch{n.followingNote.hidden=!0}},J=(e,t)=>{let n=document.createElement(`button`);n.type=`button`,n.className=`ghost follow`,n.textContent=`Follow`,n.setAttribute(`aria-label`,`Follow ${t}`);let r=e=>{n.textContent=e?`Following`:`Follow`,n.dataset.following=e?`yes`:`no`};return(async()=>{try{let t=await fetch(`/api/v1/follows/${encodeURIComponent(e)}`);t.ok&&r((await t.json()).following===!0)}catch{}})(),n.addEventListener(`click`,()=>{(async()=>{let t=n.dataset.following===`yes`;n.disabled=!0;try{(await fetch(`/api/v1/follows/${encodeURIComponent(e)}`,{method:t?`DELETE`:`PUT`,headers:{"content-type":`application/json`},body:t?void 0:`{}`})).ok&&(r(!t),me())}catch{}finally{n.disabled=!1}})()}),n},he=e=>{let t=(e+`=`.repeat((4-e.length%4)%4)).replace(/-/g,`+`).replace(/_/g,`/`),n=atob(t),r=new Uint8Array(new ArrayBuffer(n.length));for(let e=0;e<n.length;e+=1)r[e]=n.charCodeAt(e);return r},ge=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,_e=async()=>{if(!ge())return n.notifyNote.textContent=`This browser cannot show notifications.`,!1;if(Notification.permission===`denied`)return n.notifyNote.textContent=`This browser is blocking notifications. Allow them in site settings first.`,!1;if(await Notification.requestPermission()!==`granted`)return n.notifyNote.textContent=`Not allowed, so nothing will be sent here.`,!1;try{let e=await navigator.serviceWorker.ready,{publicKey:t}=await(await fetch(`/api/v1/notify/key`)).json();if(!t)return n.notifyNote.textContent=`This server is not set up to send notifications.`,!1;let r=await e.pushManager.getSubscription()??await e.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:he(t)}),i=await fetch(`/api/v1/notify/subscribe`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r.toJSON())});if(!i.ok)throw Error(String(i.status));return n.notifyNote.textContent=`This device will be told.`,!0}catch{return n.notifyNote.textContent=`Could not set this device up.`,!1}},ve=async()=>{try{let e=await(await navigator.serviceWorker.ready).pushManager.getSubscription();if(!e)return;await fetch(`/api/v1/notify/subscribe?endpoint=${encodeURIComponent(e.endpoint)}`,{method:`DELETE`}),await e.unsubscribe()}catch{}},Y=async e=>{try{let t=await fetch(`/api/v1/notify/prefs`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(e)}),r=await t.json();n.notifyPhoneNote.textContent=t.ok?``:r.error??`that did not save`,t.ok&&typeof r.phone==`string`&&(n.notifyPhone.value=r.phone)}catch{n.notifyPhoneNote.textContent=`could not reach nixamp.com`}},ye=async()=>{try{let e=await fetch(`/api/v1/notify/prefs`);if(!e.ok)return;let t=await e.json();n.notifyEmail.checked=t.wantsEmail!==!1,n.notifySms.checked=t.wantsSms===!0,n.notifyPhone.value=t.phone??``;let r=ge()&&Notification.permission===`granted`?await(await navigator.serviceWorker.ready).pushManager.getSubscription()!==null:!1;n.notifyWeb.checked=t.wantsWeb!==!1&&r,n.notifyNote.textContent=r?`Get told when someone you follow goes live.`:`Turn on “On this device” to be told here.`}catch{}};n.notifyWeb.addEventListener(`change`,()=>{(async()=>{if(n.notifyWeb.checked){let e=await _e();n.notifyWeb.checked=e,await Y({wantsWeb:e});return}await ve(),await Y({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{Y({wantsEmail:n.notifyEmail.checked})}),n.notifySms.addEventListener(`change`,()=>{(async()=>{if(n.notifySms.checked&&!n.notifyPhone.value.trim()){n.notifyPhoneNote.textContent=`Add a phone number first.`,n.notifySms.checked=!1,n.notifyPhone.focus();return}await Y({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),Y({phone:n.notifyPhone.value.trim()})});let X=!1,Z=``,Q=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(ye(),me()):(n.followingNote.hidden=!0,n.followingList.replaceChildren(),n.recentNote.hidden=!0,n.recentList.replaceChildren()),n.accountForm.hidden=t,n.accountProviders.hidden=t||n.accountProviders.childElementCount===0,n.accountSignOut.hidden=!t,n.accountNote.textContent=t?`Signed in as ${e}.`:X?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=X?`Create account`:`Sign in`,n.accountToggle.textContent=X?`I have one`:`Create one`,n.accountPassword.autocomplete=X?`new-password`:`current-password`};n.accountToggle.addEventListener(`click`,()=>{X=!X,Q(null)}),n.accountForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.accountEmail.value.trim(),r=n.accountPassword.value;(async()=>{n.accountSubmit.disabled=!0;try{let e=await fetch(`/api/v1/auth/${X?`signup`:`login`}`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({email:t,password:r})}),i=await e.json();if(!e.ok){n.accountNote.textContent=i.error??`that did not work`;return}Z=i.account?.id??``,n.accountPassword.value=``,Q(i.account?.email??t),K()}catch{n.accountNote.textContent=`could not reach nixamp.com`}finally{n.accountSubmit.disabled=!1}})()}),n.accountSignOut.addEventListener(`click`,()=>{(async()=>{try{await fetch(`/api/v1/auth/logout`,{method:`POST`})}catch{}Z=``,Q(null),K()})()}),(async()=>{let e=[],t=!1;try{let n=await fetch(`/api/v1/auth/providers`);n.ok&&(t=!0,e=(await n.json()).providers??[])}catch{}n.accountProviders.replaceChildren(),n.accountProviders.hidden=e.length===0,n.accountPanel.hidden=!t,n.accountElsewhere.hidden=t;for(let t of e){let e=document.createElement(`a`);e.className=`button`,e.href=`/api/v1/${encodeURIComponent(t.id)}/oauth/start`,e.textContent=`Continue with ${t.name}`,n.accountProviders.append(e)}})(),(async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();Z=e.ok?t.account?.id??``:``,Q(e.ok?t.account?.email??`you`:null)}catch{Z=``,Q(null)}})(),K(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}U(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{g.close(),r=`local`,s=`idle`,c=``,L()}),n.listenHere.addEventListener(`change`,()=>{r===`remote`&&(async()=>{n.listenHere.checked?(await g.send({type:`stop`}),await O(o.index)):h.stop(),L()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),k();return;case`s`:j();return;case`n`:case`ArrowRight`:A(1);return;case`p`:case`ArrowLeft`:A(-1);return;case`ArrowDown`:e.preventDefault(),D(Math.min(_()-1,v()+1));return;case`ArrowUp`:e.preventDefault(),D(Math.max(0,v()-1));return}});let $=null;globalThis.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),$=e,n.install.hidden=!1}),n.install.addEventListener(`click`,()=>{$?.prompt(),$=null,n.install.hidden=!0});try{let e=localStorage.getItem(F);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),h.volume=Number(e));let t=localStorage.getItem(P);t&&(n.remoteUrl.value=t)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await ae(e)===null)return;let t=await ie(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,r=`remote`,l=``,g.connect(e),L())})(),L(),requestAnimationFrame(z)}L(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
|