nixamp 0.5.10 → 0.5.11

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 CHANGED
@@ -23,7 +23,11 @@ import { KEY_HEADER } from "./share.js";
23
23
  const RECONNECT_MS = 1000;
24
24
  /** A remote track has no path, because no filesystem path leaves the machine. */
25
25
  export function applySnapshot(state, snapshot) {
26
- state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
26
+ // A frame without a list is not an empty library, it is a frame with nothing
27
+ // new to say about it -- which is every frame but the first.
28
+ if (snapshot.tracks !== undefined) {
29
+ state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
30
+ }
27
31
  state.index = snapshot.index;
28
32
  state.playing = snapshot.playing;
29
33
  state.position = snapshot.position;
@@ -25,7 +25,24 @@ export interface RemoteTrack {
25
25
  export interface Snapshot {
26
26
  /** Bumped on every push so a client can drop an out-of-order frame. */
27
27
  revision: number;
28
- tracks: RemoteTrack[];
28
+ /**
29
+ * The library, sent when it is news and left out when it is not.
30
+ *
31
+ * It used to ride in every frame. At twelve frames a second over a library of
32
+ * five thousand, that is five megabytes a second of JSON for a client to
33
+ * parse on the thread that is also decoding the audio -- which is exactly
34
+ * what it sounded like. It is sent on the first frame of a subscription and
35
+ * again whenever the list actually changes; absent means "the same as
36
+ * before", and a client keeps what it had.
37
+ */
38
+ tracks?: RemoteTrack[];
39
+ /**
40
+ * How many tracks there are, in every frame.
41
+ *
42
+ * A client that has not received a list yet still has to draw something, and
43
+ * a count is four bytes rather than half a megabyte.
44
+ */
45
+ trackCount: number;
29
46
  index: number;
30
47
  playing: boolean;
31
48
  position: number;
@@ -61,4 +78,19 @@ export declare const COMMAND_TYPES: readonly ["play", "toggle", "stop", "next",
61
78
  export declare function parseCommand(input: unknown): Command | null;
62
79
  /** The name a remote shows for a track. */
63
80
  export declare function remoteName(track: RemoteTrack): string;
64
- export declare function emptySnapshot(): Snapshot;
81
+ export declare function emptySnapshot(): FullSnapshot;
82
+ /**
83
+ * Fold a frame into what the client already had.
84
+ *
85
+ * A frame without `tracks` is not a frame with no tracks: it is a frame that
86
+ * had nothing new to say about them. Every client needs this, so none of them
87
+ * should write it twice.
88
+ */
89
+ export declare function merge(previous: FullSnapshot, incoming: Snapshot): FullSnapshot;
90
+ /**
91
+ * A snapshot a client has already folded, so the library is known to be there.
92
+ * Every reader wants this one; only the wire carries the other.
93
+ */
94
+ export type FullSnapshot = Snapshot & {
95
+ tracks: RemoteTrack[];
96
+ };
package/dist/protocol.js CHANGED
@@ -41,6 +41,7 @@ export function emptySnapshot() {
41
41
  return {
42
42
  revision: 0,
43
43
  tracks: [],
44
+ trackCount: 0,
44
45
  index: 0,
45
46
  playing: false,
46
47
  position: 0,
@@ -51,3 +52,13 @@ export function emptySnapshot() {
51
52
  root: "",
52
53
  };
53
54
  }
55
+ /**
56
+ * Fold a frame into what the client already had.
57
+ *
58
+ * A frame without `tracks` is not a frame with no tracks: it is a frame that
59
+ * had nothing new to say about them. Every client needs this, so none of them
60
+ * should write it twice.
61
+ */
62
+ export function merge(previous, incoming) {
63
+ return { ...incoming, tracks: incoming.tracks ?? previous.tracks };
64
+ }
package/dist/server.d.ts CHANGED
@@ -111,7 +111,8 @@ export declare function parseRange(header: string | undefined, size: number): By
111
111
  export declare function safeJoin(rootDir: string, urlPath: string): string | null;
112
112
  /** What the HTTP layer needs from a player. Tests hand it a fake. */
113
113
  export interface Engine {
114
- snapshot(): Snapshot;
114
+ /** `withTracks` false leaves the library out, for a frame that is only motion. */
115
+ snapshot(withTracks?: boolean): Snapshot;
115
116
  command(command: Command): void;
116
117
  subscribe(listener: (snapshot: Snapshot) => void): () => void;
117
118
  /** Absolute path of a track, or undefined when the index is not one. */
@@ -161,7 +162,11 @@ export declare class PlayerEngine implements Engine {
161
162
  fps?: number);
162
163
  private readonly silent;
163
164
  private consume;
164
- snapshot(): Snapshot;
165
+ /**
166
+ * The current state. `withTracks` carries the library, which is worth half a
167
+ * megabyte on a real one and is only news when it has changed.
168
+ */
169
+ snapshot(withTracks?: boolean): Snapshot;
165
170
  trackPath(index: number): string | undefined;
166
171
  command(command: Command): void;
167
172
  private clamp;
@@ -169,6 +174,13 @@ export declare class PlayerEngine implements Engine {
169
174
  private start;
170
175
  private halt;
171
176
  subscribe(listener: (snapshot: Snapshot) => void): () => void;
177
+ /**
178
+ * Send the state to everyone watching.
179
+ *
180
+ * The library goes only when `listChanged` says it has, which is what turned
181
+ * five megabytes a second into a few kilobytes: an analyser tick has nothing
182
+ * to say about the track list, and it fires twelve times a second.
183
+ */
172
184
  private push;
173
185
  stop(): void;
174
186
  replace(tracks: Track[], root: string): void;
package/dist/server.js CHANGED
@@ -348,10 +348,15 @@ export class PlayerEngine {
348
348
  this.pending = joined.subarray(at);
349
349
  this.dirty = true;
350
350
  }
351
- snapshot() {
351
+ /**
352
+ * The current state. `withTracks` carries the library, which is worth half a
353
+ * megabyte on a real one and is only news when it has changed.
354
+ */
355
+ snapshot(withTracks = true) {
352
356
  return {
353
357
  revision: this.revision,
354
- tracks: toRemoteTracks(this.tracks),
358
+ ...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
359
+ trackCount: this.tracks.length,
355
360
  index: this.state.index,
356
361
  playing: this.state.playing,
357
362
  position: this.state.position,
@@ -445,11 +450,18 @@ export class PlayerEngine {
445
450
  }
446
451
  };
447
452
  }
448
- push() {
453
+ /**
454
+ * Send the state to everyone watching.
455
+ *
456
+ * The library goes only when `listChanged` says it has, which is what turned
457
+ * five megabytes a second into a few kilobytes: an analyser tick has nothing
458
+ * to say about the track list, and it fires twelve times a second.
459
+ */
460
+ push(listChanged = false) {
449
461
  this.revision++;
450
462
  if (this.listeners.size === 0)
451
463
  return;
452
- const snapshot = this.snapshot();
464
+ const snapshot = this.snapshot(listChanged);
453
465
  for (const listener of this.listeners)
454
466
  listener(snapshot);
455
467
  }
@@ -468,7 +480,7 @@ export class PlayerEngine {
468
480
  this.state.index = 0;
469
481
  this.state.position = 0;
470
482
  this.state.note = "";
471
- this.push();
483
+ this.push(true);
472
484
  }
473
485
  retag(tracks, root) {
474
486
  // Dropped rather than applied if the library moved underneath: somebody
@@ -480,8 +492,9 @@ export class PlayerEngine {
480
492
  return;
481
493
  this.tracks = tracks;
482
494
  // No stop, no index reset: the only thing that changes is what the titles
483
- // say, and every remote finds out because a snapshot goes out.
484
- this.push();
495
+ // say, and every remote finds out because a snapshot goes out -- carrying
496
+ // the list, since the titles are the whole point of this one.
497
+ this.push(true);
485
498
  }
486
499
  }
487
500
  /** An engine with no library behind it, for the hosted PWA. */
@@ -1540,7 +1553,7 @@ export function createHandler(engine, options) {
1540
1553
  json(response, 404, { error: "no such track" });
1541
1554
  return;
1542
1555
  }
1543
- watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
1556
+ watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
1544
1557
  // A browser asks for every track here, and a matroska or an avi handed
1545
1558
  // to it raw is bytes it cannot play. Seeking is what this route is for
1546
1559
  // and transcoding gives it up, but an unseekable film beats a silent
@@ -1572,11 +1585,11 @@ export function createHandler(engine, options) {
1572
1585
  return;
1573
1586
  }
1574
1587
  const current = engine.snapshot();
1575
- if (current.tracks.length === 0) {
1588
+ if (current.trackCount === 0) {
1576
1589
  json(response, 404, { error: "nothing is playing" });
1577
1590
  return;
1578
1591
  }
1579
- watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
1592
+ watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
1580
1593
  liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
1581
1594
  return;
1582
1595
  }
@@ -1591,7 +1604,7 @@ export function createHandler(engine, options) {
1591
1604
  json(response, 403, { error: "media streaming is off" });
1592
1605
  return;
1593
1606
  }
1594
- watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
1607
+ watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
1595
1608
  transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
1596
1609
  return;
1597
1610
  }
@@ -2341,7 +2354,7 @@ export async function serve(argv, version = "0.1.0") {
2341
2354
  },
2342
2355
  nowPlaying: () => {
2343
2356
  const snapshot = engine.snapshot();
2344
- return snapshot.tracks[snapshot.index]?.title ?? "";
2357
+ return snapshot.tracks?.[snapshot.index]?.title ?? "";
2345
2358
  },
2346
2359
  onConfig: (remote) => {
2347
2360
  const next = applyRemoteConfig(paywallConfig, remote?.x402);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/attach.ts CHANGED
@@ -26,7 +26,11 @@ const RECONNECT_MS = 1000;
26
26
 
27
27
  /** A remote track has no path, because no filesystem path leaves the machine. */
28
28
  export function applySnapshot(state: State, snapshot: Snapshot): void {
29
- state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
29
+ // A frame without a list is not an empty library, it is a frame with nothing
30
+ // new to say about it -- which is every frame but the first.
31
+ if (snapshot.tracks !== undefined) {
32
+ state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
33
+ }
30
34
  state.index = snapshot.index;
31
35
  state.playing = snapshot.playing;
32
36
  state.position = snapshot.position;
package/src/protocol.ts CHANGED
@@ -27,7 +27,24 @@ export interface RemoteTrack {
27
27
  export interface Snapshot {
28
28
  /** Bumped on every push so a client can drop an out-of-order frame. */
29
29
  revision: number;
30
- tracks: RemoteTrack[];
30
+ /**
31
+ * The library, sent when it is news and left out when it is not.
32
+ *
33
+ * It used to ride in every frame. At twelve frames a second over a library of
34
+ * five thousand, that is five megabytes a second of JSON for a client to
35
+ * parse on the thread that is also decoding the audio -- which is exactly
36
+ * what it sounded like. It is sent on the first frame of a subscription and
37
+ * again whenever the list actually changes; absent means "the same as
38
+ * before", and a client keeps what it had.
39
+ */
40
+ tracks?: RemoteTrack[];
41
+ /**
42
+ * How many tracks there are, in every frame.
43
+ *
44
+ * A client that has not received a list yet still has to draw something, and
45
+ * a count is four bytes rather than half a megabyte.
46
+ */
47
+ trackCount: number;
31
48
  index: number;
32
49
  playing: boolean;
33
50
  position: number;
@@ -83,10 +100,11 @@ export function remoteName(track: RemoteTrack): string {
83
100
  return track.artist ? `${track.artist} — ${track.title}` : track.title;
84
101
  }
85
102
 
86
- export function emptySnapshot(): Snapshot {
103
+ export function emptySnapshot(): FullSnapshot {
87
104
  return {
88
105
  revision: 0,
89
106
  tracks: [],
107
+ trackCount: 0,
90
108
  index: 0,
91
109
  playing: false,
92
110
  position: 0,
@@ -97,3 +115,20 @@ export function emptySnapshot(): Snapshot {
97
115
  root: "",
98
116
  };
99
117
  }
118
+
119
+ /**
120
+ * Fold a frame into what the client already had.
121
+ *
122
+ * A frame without `tracks` is not a frame with no tracks: it is a frame that
123
+ * had nothing new to say about them. Every client needs this, so none of them
124
+ * should write it twice.
125
+ */
126
+ export function merge(previous: FullSnapshot, incoming: Snapshot): FullSnapshot {
127
+ return { ...incoming, tracks: incoming.tracks ?? previous.tracks };
128
+ }
129
+
130
+ /**
131
+ * A snapshot a client has already folded, so the library is known to be there.
132
+ * Every reader wants this one; only the wire carries the other.
133
+ */
134
+ export type FullSnapshot = Snapshot & { tracks: RemoteTrack[] };
package/src/server.ts CHANGED
@@ -362,7 +362,8 @@ export function safeJoin(rootDir: string, urlPath: string): string | null {
362
362
 
363
363
  /** What the HTTP layer needs from a player. Tests hand it a fake. */
364
364
  export interface Engine {
365
- snapshot(): Snapshot;
365
+ /** `withTracks` false leaves the library out, for a frame that is only motion. */
366
+ snapshot(withTracks?: boolean): Snapshot;
366
367
  command(command: Command): void;
367
368
  subscribe(listener: (snapshot: Snapshot) => void): () => void;
368
369
  /** Absolute path of a track, or undefined when the index is not one. */
@@ -476,10 +477,15 @@ export class PlayerEngine implements Engine {
476
477
  this.dirty = true;
477
478
  }
478
479
 
479
- snapshot(): Snapshot {
480
+ /**
481
+ * The current state. `withTracks` carries the library, which is worth half a
482
+ * megabyte on a real one and is only news when it has changed.
483
+ */
484
+ snapshot(withTracks = true): Snapshot {
480
485
  return {
481
486
  revision: this.revision,
482
- tracks: toRemoteTracks(this.tracks),
487
+ ...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
488
+ trackCount: this.tracks.length,
483
489
  index: this.state.index,
484
490
  playing: this.state.playing,
485
491
  position: this.state.position,
@@ -569,10 +575,17 @@ export class PlayerEngine implements Engine {
569
575
  };
570
576
  }
571
577
 
572
- private push(): void {
578
+ /**
579
+ * Send the state to everyone watching.
580
+ *
581
+ * The library goes only when `listChanged` says it has, which is what turned
582
+ * five megabytes a second into a few kilobytes: an analyser tick has nothing
583
+ * to say about the track list, and it fires twelve times a second.
584
+ */
585
+ private push(listChanged = false): void {
573
586
  this.revision++;
574
587
  if (this.listeners.size === 0) return;
575
- const snapshot = this.snapshot();
588
+ const snapshot = this.snapshot(listChanged);
576
589
  for (const listener of this.listeners) listener(snapshot);
577
590
  }
578
591
 
@@ -592,7 +605,7 @@ export class PlayerEngine implements Engine {
592
605
  this.state.index = 0;
593
606
  this.state.position = 0;
594
607
  this.state.note = "";
595
- this.push();
608
+ this.push(true);
596
609
  }
597
610
 
598
611
  retag(tracks: Track[], root: string): void {
@@ -603,8 +616,9 @@ export class PlayerEngine implements Engine {
603
616
  if (tracks.some((track, at) => track.path !== this.tracks[at]?.path)) return;
604
617
  this.tracks = tracks;
605
618
  // 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();
619
+ // say, and every remote finds out because a snapshot goes out -- carrying
620
+ // the list, since the titles are the whole point of this one.
621
+ this.push(true);
608
622
  }
609
623
  }
610
624
 
@@ -614,6 +628,7 @@ export class EmptyEngine implements Engine {
614
628
  snapshot(): Snapshot {
615
629
  return { ...emptySnapshot(), note: this.note };
616
630
  }
631
+
617
632
  command(): void {}
618
633
  subscribe(listener: (snapshot: Snapshot) => void): () => void {
619
634
  listener(this.snapshot());
@@ -1811,7 +1826,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1811
1826
  json(response, 404, { error: "no such track" });
1812
1827
  return;
1813
1828
  }
1814
- watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
1829
+ watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
1815
1830
  // A browser asks for every track here, and a matroska or an avi handed
1816
1831
  // to it raw is bytes it cannot play. Seeking is what this route is for
1817
1832
  // and transcoding gives it up, but an unseekable film beats a silent
@@ -1842,11 +1857,11 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1842
1857
  return;
1843
1858
  }
1844
1859
  const current = engine.snapshot();
1845
- if (current.tracks.length === 0) {
1860
+ if (current.trackCount === 0) {
1846
1861
  json(response, 404, { error: "nothing is playing" });
1847
1862
  return;
1848
1863
  }
1849
- watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
1864
+ watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
1850
1865
  liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
1851
1866
  return;
1852
1867
  }
@@ -1862,7 +1877,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1862
1877
  json(response, 403, { error: "media streaming is off" });
1863
1878
  return;
1864
1879
  }
1865
- watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
1880
+ watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
1866
1881
  transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
1867
1882
  return;
1868
1883
  }
@@ -2681,7 +2696,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2681
2696
  },
2682
2697
  nowPlaying: () => {
2683
2698
  const snapshot = engine.snapshot();
2684
- return snapshot.tracks[snapshot.index]?.title ?? "";
2699
+ return snapshot.tracks?.[snapshot.index]?.title ?? "";
2685
2700
  },
2686
2701
  onConfig: (remote) => {
2687
2702
  const next = applyRemoteConfig(paywallConfig, (remote as { x402?: unknown })?.x402);
@@ -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-CRfW9BIP.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-Bug6MFON.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-DW1kEon1.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 ee(e){return e.filter(e=>_(e.name,e.type)).sort((e,t)=>v(y(e),y(t))).map(e=>({title:n(e.name),artist:``,album:b(y(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function y(e){return e.webkitRelativePath||e.name}function b(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function x(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}}var j=.14,M=.02;function oe(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 se(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 ce(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 le(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,y=()=>{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.`},b=()=>(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=y(),n.album.textContent=b();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===``,ue(),n.glyphs.textContent=d.map(M).join(``);let[ee,x]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(x*6)).padEnd(6,`·`)}`}let R=``;function ue(){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=oe(24,e.length)),d=ce(d,se(e,p)),f=N(f,d))}if(s){let e=getComputedStyle(document.documentElement);le(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:y(),album:b(),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=ee(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,L();return}x(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()=>{if(s=`connecting`,L(),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??[],fe(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(pe(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,de=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.`,de(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`},fe=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,pe(e.ownerId,e.name)),n.recentList.append(t)}},J=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}},pe=(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),J())}catch{}finally{n.disabled=!1}})()}),n},me=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},he=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,ge=async()=>{if(!he())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:me(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}},_e=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`}},ve=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=he()&&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 ge();n.notifyWeb.checked=e,await Y({wantsWeb:e});return}await _e(),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?(ve(),J()):(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};