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/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
- snapshot(): Snapshot;
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
- snapshot(): Snapshot {
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
- private push(): void {
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.push();
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.tracks.length === 0) {
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
- return createHttpServer((request, response) => {
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(options.host, port, `http://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`)
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 `http://${authority}:${port}`;
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: `http://localhost:${port}` },
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-ABOb54bx.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};
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};