nixamp 0.7.22 → 0.7.24

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.
@@ -28,6 +28,16 @@ export interface RemoteTrack {
28
28
  * a client puts at the top of that block so the two are not one soup.
29
29
  */
30
30
  group?: string;
31
+ /**
32
+ * Which folder it sits in, under whatever it was loaded from.
33
+ *
34
+ * Empty for a track at the top. A library is a shelf of albums and seasons,
35
+ * and five thousand files in one flat list is a list nobody can find
36
+ * anything in -- so the shape of the folders comes across and a player can
37
+ * offer them as folders. Relative, always: where the library sits on
38
+ * somebody's disk is their business.
39
+ */
40
+ folder?: string;
31
41
  }
32
42
  /** Everything a remote needs to draw the player. */
33
43
  export interface Snapshot {
package/dist/server.d.ts CHANGED
@@ -136,6 +136,8 @@ export declare function safeJoin(rootDir: string, urlPath: string): string | nul
136
136
  */
137
137
  export type Loaded = Track & {
138
138
  group?: string;
139
+ /** The folder it sits in, relative to what it was loaded from. */
140
+ folder?: string;
139
141
  /**
140
142
  * Whether this has a picture, when the name could not say.
141
143
  *
@@ -205,6 +207,17 @@ export interface Engine {
205
207
  }
206
208
  export declare function toRemoteTracks(tracks: Loaded[]): RemoteTrack[];
207
209
  export declare function hasPicture(path: string): boolean;
210
+ /**
211
+ * Where a track sits, relative to the thing it was loaded from.
212
+ *
213
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
214
+ * files is one nobody can find anything in. This is what lets a player offer
215
+ * the folders as folders.
216
+ *
217
+ * Relative and never absolute: the shape of somebody's library is what a
218
+ * listener needs, and where it lives on their disk is not.
219
+ */
220
+ export declare function folderOf(path: string, from: string): string;
208
221
  /**
209
222
  * Whether the name of a source tells us anything about what is inside it.
210
223
  *
@@ -373,6 +386,18 @@ export interface HandlerOptions {
373
386
  };
374
387
  /** What this server calls itself, for the list of what is live on it. */
375
388
  serverName?: string;
389
+ /**
390
+ * The source this server was started on -- its own library.
391
+ *
392
+ * Replacing the playlist with a stream leaves no way back to it: the address
393
+ * is a path on somebody else's machine, and a person looking at a player has
394
+ * no reason to know it. Reported to an administrator so there can be a
395
+ * button rather than a thing you have to remember and retype.
396
+ *
397
+ * Admin-only, because it is a filesystem path and a viewer has no business
398
+ * with it.
399
+ */
400
+ homeSource?: string;
376
401
  /**
377
402
  * Going live: whether this server is listed, and how to change that.
378
403
  *
package/dist/server.js CHANGED
@@ -302,6 +302,7 @@ export function toRemoteTracks(tracks) {
302
302
  // Only for what was added; the library's own tracks say nothing, which is
303
303
  // how a client knows they are the library.
304
304
  ...(t.group ? { group: t.group } : {}),
305
+ ...(t.folder ? { folder: t.folder } : {}),
305
306
  }));
306
307
  }
307
308
  /** Video containers, as opposed to the songs that are most of a library. */
@@ -310,6 +311,34 @@ export function hasPicture(path) {
310
311
  const dot = path.lastIndexOf(".");
311
312
  return dot > 0 && PICTURE.has(path.slice(dot).toLowerCase());
312
313
  }
314
+ /**
315
+ * Where a track sits, relative to the thing it was loaded from.
316
+ *
317
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
318
+ * files is one nobody can find anything in. This is what lets a player offer
319
+ * the folders as folders.
320
+ *
321
+ * Relative and never absolute: the shape of somebody's library is what a
322
+ * listener needs, and where it lives on their disk is not.
323
+ */
324
+ export function folderOf(path, from) {
325
+ const strip = (value) => value.replace(/\/+$/, "");
326
+ const base = strip(from);
327
+ if (base === "" || !path.startsWith(base + "/"))
328
+ return "";
329
+ const rest = path.slice(base.length + 1);
330
+ const at = rest.lastIndexOf("/");
331
+ if (at === -1)
332
+ return "";
333
+ const folder = rest.slice(0, at);
334
+ // A URL's path is percent-encoded and a person reading a folder name is not.
335
+ try {
336
+ return isRemote(path) ? decodeURIComponent(folder) : folder;
337
+ }
338
+ catch {
339
+ return folder;
340
+ }
341
+ }
313
342
  /**
314
343
  * Whether the name of a source tells us anything about what is inside it.
315
344
  *
@@ -521,7 +550,7 @@ export class PlayerEngine {
521
550
  }
522
551
  replace(tracks, root) {
523
552
  this.stop();
524
- this.tracks = tracks;
553
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
525
554
  this.root = root;
526
555
  this.state.index = 0;
527
556
  this.state.position = 0;
@@ -544,7 +573,9 @@ export class PlayerEngine {
544
573
  add(tracks, from) {
545
574
  const group = sourceLabel(from);
546
575
  const known = new Set(this.tracks.map((track) => track.path));
547
- const fresh = tracks.filter((track) => !known.has(track.path)).map((track) => ({ ...track, group }));
576
+ const fresh = tracks
577
+ .filter((track) => !known.has(track.path))
578
+ .map((track) => ({ ...track, group, folder: folderOf(track.path, from) }));
548
579
  if (fresh.length === 0)
549
580
  return 0;
550
581
  this.tracks = [...this.tracks, ...fresh];
@@ -617,7 +648,7 @@ export class PlayerEngine {
617
648
  // somebody pointed the server elsewhere. Theirs wins.
618
649
  if (this.tracks.length > 0)
619
650
  return;
620
- this.tracks = tracks;
651
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
621
652
  this.root = root;
622
653
  this.state.note = tracks.length === 0 ? `No audio files under ${root}.` : "";
623
654
  this.push(true);
@@ -639,7 +670,11 @@ export class PlayerEngine {
639
670
  changed = true;
640
671
  // The group is ours, not the tagger's: it knows what a track is called,
641
672
  // not which pile it is in.
642
- return { ...tagged, ...(track.group ? { group: track.group } : {}) };
673
+ return {
674
+ ...tagged,
675
+ ...(track.group ? { group: track.group } : {}),
676
+ ...(track.folder ? { folder: track.folder } : {}),
677
+ };
643
678
  });
644
679
  if (!changed)
645
680
  return;
@@ -1692,6 +1727,16 @@ export function createHandler(engine, options) {
1692
1727
  listeners: one.listeners,
1693
1728
  startedAt: one.startedAt,
1694
1729
  })),
1730
+ // Anything re-streamed into this server is a live stream too, and was
1731
+ // sitting in the middle of the playlist among the files -- which is
1732
+ // what made moving between a channel and an album so confusing. Named
1733
+ // here with the first track it owns, so it can be played from the list
1734
+ // of what is live rather than hunted for among five thousand files.
1735
+ restreams: engine.groups().map((name) => ({
1736
+ name,
1737
+ at: (engine.snapshot().tracks ?? []).findIndex((track) => track.group === name),
1738
+ tracks: (engine.snapshot().tracks ?? []).filter((track) => track.group === name).length,
1739
+ })),
1695
1740
  });
1696
1741
  return;
1697
1742
  }
@@ -1938,6 +1983,10 @@ export function createHandler(engine, options) {
1938
1983
  // And which of those slots somebody is already on, because the
1939
1984
  // question you have in front of three addresses is which one is free.
1940
1985
  channels: options.channels?.list().map(({ id, name, via }) => ({ id, name, via })) ?? [],
1986
+ // What this server's own library is, and whether it is loaded, so
1987
+ // there can be a way back to it that is not retyping a path.
1988
+ home: options.homeSource ?? "",
1989
+ root: engine.snapshot(false).root,
1941
1990
  });
1942
1991
  return;
1943
1992
  }
@@ -2723,6 +2772,7 @@ export async function serve(argv, version = "0.1.0") {
2723
2772
  channels,
2724
2773
  publishUrls: () => publishUrls,
2725
2774
  serverName: options.name || hostname(),
2775
+ homeSource: root,
2726
2776
  live: {
2727
2777
  status: () => ({
2728
2778
  live: publisher !== null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.7.22",
3
+ "version": "0.7.24",
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/protocol.ts CHANGED
@@ -29,6 +29,16 @@ export interface RemoteTrack {
29
29
  * a client puts at the top of that block so the two are not one soup.
30
30
  */
31
31
  group?: string;
32
+ /**
33
+ * Which folder it sits in, under whatever it was loaded from.
34
+ *
35
+ * Empty for a track at the top. A library is a shelf of albums and seasons,
36
+ * and five thousand files in one flat list is a list nobody can find
37
+ * anything in -- so the shape of the folders comes across and a player can
38
+ * offer them as folders. Relative, always: where the library sits on
39
+ * somebody's disk is their business.
40
+ */
41
+ folder?: string;
32
42
  }
33
43
 
34
44
  /** Everything a remote needs to draw the player. */
package/src/server.ts CHANGED
@@ -408,6 +408,8 @@ export function safeJoin(rootDir: string, urlPath: string): string | null {
408
408
  */
409
409
  export type Loaded = Track & {
410
410
  group?: string;
411
+ /** The folder it sits in, relative to what it was loaded from. */
412
+ folder?: string;
411
413
  /**
412
414
  * Whether this has a picture, when the name could not say.
413
415
  *
@@ -491,6 +493,7 @@ export function toRemoteTracks(tracks: Loaded[]): RemoteTrack[] {
491
493
  // Only for what was added; the library's own tracks say nothing, which is
492
494
  // how a client knows they are the library.
493
495
  ...(t.group ? { group: t.group } : {}),
496
+ ...(t.folder ? { folder: t.folder } : {}),
494
497
  }));
495
498
  }
496
499
 
@@ -502,6 +505,32 @@ export function hasPicture(path: string): boolean {
502
505
  return dot > 0 && PICTURE.has(path.slice(dot).toLowerCase());
503
506
  }
504
507
 
508
+ /**
509
+ * Where a track sits, relative to the thing it was loaded from.
510
+ *
511
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
512
+ * files is one nobody can find anything in. This is what lets a player offer
513
+ * the folders as folders.
514
+ *
515
+ * Relative and never absolute: the shape of somebody's library is what a
516
+ * listener needs, and where it lives on their disk is not.
517
+ */
518
+ export function folderOf(path: string, from: string): string {
519
+ const strip = (value: string): string => value.replace(/\/+$/, "");
520
+ const base = strip(from);
521
+ if (base === "" || !path.startsWith(base + "/")) return "";
522
+ const rest = path.slice(base.length + 1);
523
+ const at = rest.lastIndexOf("/");
524
+ if (at === -1) return "";
525
+ const folder = rest.slice(0, at);
526
+ // A URL's path is percent-encoded and a person reading a folder name is not.
527
+ try {
528
+ return isRemote(path) ? decodeURIComponent(folder) : folder;
529
+ } catch {
530
+ return folder;
531
+ }
532
+ }
533
+
505
534
  /**
506
535
  * Whether the name of a source tells us anything about what is inside it.
507
536
  *
@@ -711,7 +740,7 @@ export class PlayerEngine implements Engine {
711
740
 
712
741
  replace(tracks: Track[], root: string): void {
713
742
  this.stop();
714
- this.tracks = tracks;
743
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
715
744
  this.root = root;
716
745
  this.state.index = 0;
717
746
  this.state.position = 0;
@@ -735,7 +764,9 @@ export class PlayerEngine implements Engine {
735
764
  add(tracks: Track[], from: string): number {
736
765
  const group = sourceLabel(from);
737
766
  const known = new Set(this.tracks.map((track) => track.path));
738
- const fresh = tracks.filter((track) => !known.has(track.path)).map((track) => ({ ...track, group }));
767
+ const fresh = tracks
768
+ .filter((track) => !known.has(track.path))
769
+ .map((track) => ({ ...track, group, folder: folderOf(track.path, from) }));
739
770
  if (fresh.length === 0) return 0;
740
771
  this.tracks = [...this.tracks, ...fresh];
741
772
  // The list itself changed, so it has to ride this frame; a count nobody
@@ -805,7 +836,7 @@ export class PlayerEngine implements Engine {
805
836
  // Something is already loaded, so this is a scan that finished after
806
837
  // somebody pointed the server elsewhere. Theirs wins.
807
838
  if (this.tracks.length > 0) return;
808
- this.tracks = tracks;
839
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
809
840
  this.root = root;
810
841
  this.state.note = tracks.length === 0 ? `No audio files under ${root}.` : "";
811
842
  this.push(true);
@@ -827,7 +858,11 @@ export class PlayerEngine implements Engine {
827
858
  changed = true;
828
859
  // The group is ours, not the tagger's: it knows what a track is called,
829
860
  // not which pile it is in.
830
- return { ...tagged, ...(track.group ? { group: track.group } : {}) };
861
+ return {
862
+ ...tagged,
863
+ ...(track.group ? { group: track.group } : {}),
864
+ ...(track.folder ? { folder: track.folder } : {}),
865
+ };
831
866
  });
832
867
  if (!changed) return;
833
868
  this.tracks = merged;
@@ -988,6 +1023,18 @@ export interface HandlerOptions {
988
1023
  broadcast?: () => { destinations: Destination[]; settings: EncoderSettings };
989
1024
  /** What this server calls itself, for the list of what is live on it. */
990
1025
  serverName?: string;
1026
+ /**
1027
+ * The source this server was started on -- its own library.
1028
+ *
1029
+ * Replacing the playlist with a stream leaves no way back to it: the address
1030
+ * is a path on somebody else's machine, and a person looking at a player has
1031
+ * no reason to know it. Reported to an administrator so there can be a
1032
+ * button rather than a thing you have to remember and retype.
1033
+ *
1034
+ * Admin-only, because it is a filesystem path and a viewer has no business
1035
+ * with it.
1036
+ */
1037
+ homeSource?: string;
991
1038
  /**
992
1039
  * Going live: whether this server is listed, and how to change that.
993
1040
  *
@@ -2068,6 +2115,16 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2068
2115
  listeners: one.listeners,
2069
2116
  startedAt: one.startedAt,
2070
2117
  })),
2118
+ // Anything re-streamed into this server is a live stream too, and was
2119
+ // sitting in the middle of the playlist among the files -- which is
2120
+ // what made moving between a channel and an album so confusing. Named
2121
+ // here with the first track it owns, so it can be played from the list
2122
+ // of what is live rather than hunted for among five thousand files.
2123
+ restreams: engine.groups().map((name) => ({
2124
+ name,
2125
+ at: (engine.snapshot().tracks ?? []).findIndex((track) => track.group === name),
2126
+ tracks: (engine.snapshot().tracks ?? []).filter((track) => track.group === name).length,
2127
+ })),
2071
2128
  });
2072
2129
  return;
2073
2130
  }
@@ -2330,6 +2387,10 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2330
2387
  // And which of those slots somebody is already on, because the
2331
2388
  // question you have in front of three addresses is which one is free.
2332
2389
  channels: options.channels?.list().map(({ id, name, via }) => ({ id, name, via })) ?? [],
2390
+ // What this server's own library is, and whether it is loaded, so
2391
+ // there can be a way back to it that is not retyping a path.
2392
+ home: options.homeSource ?? "",
2393
+ root: engine.snapshot(false).root,
2333
2394
  });
2334
2395
  return;
2335
2396
  }
@@ -3174,6 +3235,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
3174
3235
  channels,
3175
3236
  publishUrls: () => publishUrls,
3176
3237
  serverName: options.name || hostname(),
3238
+ homeSource: root,
3177
3239
  live: {
3178
3240
  status: () => ({
3179
3241
  live: publisher !== null,
@@ -1 +1 @@
1
- import{t as e}from"./index-BjUE4krU.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-CuPGZuSo.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-CYp2nKVZ.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-SQvnAUEE.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 te(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var x=2048;function S(e,t){return e||t===`hls`||t===`mpegts`}var ne=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(C(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=x,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=S(e.video,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 C(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 re(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function ie(e,t){return{...t,tracks:t.tracks??e.tracks}}function w(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 T(e,t,n=``){let r=`${e===``?``:w(e)}${t.startsWith(`/`)?t:`/${t}`}`;return n?`${r}${r.includes(`?`)?`&`:`?`}k=${encodeURIComponent(n)}`:r}function ae(e){let t=e.trim();if(t===``)return{base:``,key:``};let n;try{n=new URL(/^https?:\/\//i.test(t)?t:`http://${t}`)}catch{return{base:``,key:``}}let r=/^\/(?:admin|view|a|v)\/([^/]+)\/?$/.exec(n.pathname),i=r?.[1]??n.searchParams.get(`k`)??``;return r&&(n.pathname=`/`),n.searchParams.delete(`k`),{base:w(`${n.origin}${n.pathname}`),key:decodeURIComponent(i)}}function E(e,t,n=0,r=``){return T(e,n>0?`/api/media/${t}?kbps=${Math.round(n)}`:`/api/media/${t}`,r)}function D(e){if(typeof e!=`object`||!e)return null;let t=e,n=re(),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}:{},...typeof t.group==`string`&&t.group!==``?{group:t.group}:{},...typeof t.folder==`string`&&t.folder!==``?{folder:t.folder}:{}}}):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 oe=class{handlers;source=null;base=``;key=``;shape=`/admin/`;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}url(e){return T(this.base,e,this.key)}get shareLink(){return this.base===``?``:this.key===``?this.base:`${this.base}${this.shape}${this.key}`}get connected(){return this.source!==null}connect(e){let{base:t,key:n}=ae(e);this.close(),this.base=t,this.key=n,this.shape=/\/(?:view|v)\/[^/]+\/?$/.test(e.trim())?`/view/`:`/admin/`,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let r=new EventSource(T(t,`/api/events`,n));this.source=r,r.onopen=()=>this.handlers.onStatus(`live`),r.onmessage=e=>{let t=D(O(e.data));t&&(t.revision<this.lastRevision||(this.lastRevision=t.revision,this.handlers.onStatus(`live`),this.handlers.onSnapshot(t)))},r.onerror=()=>{this.handlers.onStatus(`error`,`reconnecting…`)}}async send(e){if(this.base===``&&!this.connected)return;let t=await fetch(T(this.base,`/api/command`,this.key),{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=D(await t.json());n&&this.handlers.onSnapshot(n)}media(e,t=0){return E(this.base,e,t,this.key)}close(){this.source?.close(),this.source=null}};function O(e){try{return JSON.parse(e)}catch{return null}}async function se(e,t,n=``){try{let r=await fetch(T(e,`/api/state`,n),{signal:t});return r.ok?D(await r.json()):null}catch{return null}}function ce(e){if(!/^https:\/\//i.test(e))return``;let t;try{t=new URL(e).hostname.replace(/^\[|\]$/g,``)}catch{return``}return/^\d{1,3}(\.\d{1,3}){3}$/.test(t)||t.includes(`:`)?`That is an https address for a bare IP, and a certificate is issued for a name — a browser refuses it before it asks anything. Use the server's name instead (the address it printed first), or connect over http.`:``}async function le(e,t=``,n){let r;try{r=await fetch(T(e,`/api/state`,t),{signal:n})}catch{return``}return r.ok?``:r.status===401?t===``?`That server needs its share link. Paste the whole link — the one with /admin/ or /view/ in it — or sign in as its owner.`:`That share link is not accepted by that server. It may have been restarted, which gives it a new one.`:r.status===403?`That link can listen but not drive this server.`:r.status===429?`That server is asking us to slow down. Try again in a moment.`:``}async function k(e,t,n=``){try{let r=await fetch(T(e,`/api/health`,n),{signal:t});if(!r.ok)return null;let i=await r.json();return i.name===`nixamp`?i.version??`unknown`:null}catch{return null}}function ue(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 A=.14,j=.02;function de(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 fe(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 pe(e,t,n=A){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function M(e,t,n=j){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function me(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 he=`nixamp.remote`,ge=`nixamp.volume`,_e=`nixamp.listenHere`;function N(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function P(){let n={status:N(`status`),source:N(`source`),install:N(`install`),video:N(`video`),audio:N(`audio`),title:N(`title-line`),album:N(`album-line`),elapsed:N(`elapsed`),total:N(`total`),seek:N(`seek`),fullscreen:N(`fullscreen`),canvas:N(`spectrum`),glyphs:N(`glyphs`),levels:N(`levels`),playlist:N(`playlist`),crumbs:N(`crumbs`),filter:N(`filter`),playlistTitle:N(`playlist-panel`),note:N(`note`),files:N(`files`),folder:N(`folder`),remoteUrl:N(`remote-url`),remoteForm:N(`remote-form`),remoteState:N(`remote-state`),disconnect:N(`disconnect`),browse:N(`browse`),accountForm:N(`account-form`),accountEmail:N(`account-email`),accountPassword:N(`account-password`),accountSubmit:N(`account-submit`),accountToggle:N(`account-toggle`),accountProviders:N(`account-providers`),accountPanel:N(`account-panel`),accountElsewhere:N(`account-elsewhere`),accountSignOut:N(`account-signout`),accountNote:N(`account-note`),adminPanel:N(`admin-panel`),adminNote:N(`admin-note`),adminSaid:N(`admin-said`),adminConnections:N(`admin-connections`),publishPanel:N(`publish-panel`),publishNote:N(`publish-note`),publishList:N(`publish-list`),adminRestream:N(`admin-restream`),adminReplace:N(`admin-replace`),adminSource:N(`admin-source`),homeNote:N(`home-note`),loadHome:N(`load-home`),directory:N(`directory`),recentNote:N(`recent-note`),recentList:N(`recent-list`),followingNote:N(`following-note`),followingList:N(`following-list`),serversPanel:N(`servers-panel`),serversNote:N(`servers-note`),serversList:N(`servers-list`),notifyPanel:N(`notify-panel`),notifyNote:N(`notify-note`),notifyWeb:N(`notify-web`),notifyEmail:N(`notify-email`),notifySms:N(`notify-sms`),notifyPhone:N(`notify-phone`),notifyPhoneForm:N(`notify-phone-form`),notifyPhoneNote:N(`notify-phone-note`),directoryNote:N(`directory-note`),directoryList:N(`directory-list`),onairPanel:N(`onair-panel`),onairNote:N(`onair-note`),onairList:N(`onair-list`),sharePanel:N(`share-panel`),shareNote:N(`share-note`),shareLink:N(`share-link`),shareCopy:N(`share-copy`),sharePhone:N(`share-phone`),shareSend:N(`share-send`),liveControls:N(`live-controls`),goLive:N(`go-live`),stopLive:N(`stop-live`),shareTo:N(`share-to`),listenOnly:N(`listen-only`),listenHere:N(`listen-here`),volume:N(`volume`),prev:N(`prev`),playPause:N(`play-pause`),stop:N(`stop`),next:N(`next`)},r=`local`,i=[],a=0,o=re(),s=`idle`,c=``,l=`Pick files, or connect to a nixamp running somewhere else.`,u=!1,d=-1,f=``,p=Array(24).fill(0),m=Array(24).fill(0),h=[],g=()=>r===`remote`&&!n.listenHere.checked,_=new ne({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),F()},onEnded:()=>A(1),onState:()=>F(),onError:e=>{l=e,F()}}),v=new oe({onSnapshot:e=>{o=ie(o,e),g()&&(p=e.bars.length>0?e.bars:p,m=M(m,p)),F()},onStatus:(e,t)=>{s=e,c=t??``,F()}}),y=()=>r===`remote`?o.tracks.length:i.length,b=()=>r===`remote`?g()||d<0?o.index:Math.min(d,Math.max(0,o.tracks.length-1)):a,x=()=>{let e=r===`remote`?o.tracks[b()]:i[b()];return e?t(e):`Nothing loaded.`},S=()=>(r===`remote`?o.tracks[b()]:i[b()])?.album||`—`,C=()=>g()?o.tracks[b()]?.duration??0:_.duration,w=()=>g()?o.position:_.position,T=()=>g()?o.playing:_.playing;async function E(e){if(r===`remote`){if(g()){await v.send({type:`play`,index:e});return}await D(e);return}let t=i[e];t&&(a=e,await _.load(t,!0),z(t.video),we(),F())}async function D(e){let t=o.tracks[e];t&&(d=e,await _.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:v.media(e,0),video:t.video===!0,objectUrl:!1},!0),z(t.video===!0),we())}async function O(){if(g()){await v.send({type:`toggle`});return}y()!==0&&(_.playing?_.pause():_.position>0?await _.play():await E(b()),F())}async function A(e){let t=y();if(t!==0){if(g()){await v.send({type:e>0?`next`:`prev`});return}await E((b()+e+t)%t)}}async function j(){if(g()){await v.send({type:`stop`});return}_.stop(),p=Array(24).fill(0),m=[...p],F()}let P=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function F(){let t=y(),a=T();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=x(),n.album.textContent=S();let d=w(),f=C();n.elapsed.textContent=e(d),n.total.textContent=f>0?e(f):`--:--`,u||(n.seek.value=String(f>0?Math.round(d/f*1e3):0),n.seek.disabled=f<=0||g()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${v.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 m=r===`remote`&&o.note!==``?o.note:l;n.note.textContent=m,n.note.hidden=m===``,R(),n.glyphs.textContent=p.map(P).join(``);let[h,ee]=g()?o.levels:_.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(h*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)}`}let I=``,ve=-1,L=``;function ye(e){if(n.crumbs.hidden=!e,!e)return;let t=L===``?[]:L.split(`/`),r=(e,t,n)=>{if(n){let t=document.createElement(`span`);return t.className=`here`,t.textContent=e,t}let r=document.createElement(`button`);return r.type=`button`,r.textContent=e,r.addEventListener(`click`,()=>{L=t,I=``,R()}),r},i=[r(`All files`,``,t.length===0)],a=``;t.forEach((e,n)=>{a=a===``?e:`${a}/${e}`;let o=document.createElement(`span`);o.textContent=`/`,i.push(o,r(e,a,n===t.length-1))}),n.crumbs.replaceChildren(...i)}function be(e,t){let n=document.createElement(`li`);n.className=`folder`;let r=document.createElement(`span`);r.className=`name`,r.textContent=`${e}/`;let i=document.createElement(`span`);return i.className=`count`,i.textContent=`${t} file${t===1?``:`s`}`,n.append(r,i),n.addEventListener(`click`,()=>{L=L===``?e:`${L}/${e}`,I=``,R()}),n}function R(){let a=r===`remote`?o.tracks.map(e=>({name:t(e),seconds:e.duration,group:e.group??``,folder:e.folder??``})):i.map(e=>({name:t(e),seconds:e.duration,group:``,folder:``})),s=n.filter.value.trim().toLowerCase(),c=a.map((e,t)=>({...e,index:t})).filter(e=>e.group===``).filter(e=>s===``||`${e.folder}/${e.name}`.toLowerCase().includes(s)),l=e=>s!==``||L===``||e===L||e.startsWith(`${L}/`),u=e=>s!==``||e===L,d=e=>{let t=L===``?e:e.slice(L.length+1),n=t.indexOf(`/`);return n===-1?t:t.slice(0,n)},f=new Map;for(let e of c){if(!l(e.folder)||u(e.folder))continue;let t=d(e.folder);t!==``&&f.set(t,(f.get(t)??0)+1)}let p=c.filter(e=>u(e.folder)&&l(e.folder)),m=`${r}:${L}:${s}:${[...f].join(`,`)}:${p.map(e=>`${e.index}@${e.name}@${e.seconds}@${e.group}`).join(`|`)}`;if(m!==I){I=m,ye(s===``&&([...f.keys()].length>0||L!==``));let t=[];for(let[e,n]of[...f].sort((e,t)=>e[0].localeCompare(t[0],void 0,{numeric:!0})))t.push(be(e,n));let r=``,i=p.some(e=>e.group!==``);for(let n of p){n.group!==r&&(i||n.group!==``)&&(r=n.group,t.push(xe(n.group)));let a=document.createElement(`li`);a.className=`row`,a.dataset.index=String(n.index);let o=document.createElement(`span`);o.className=`n`,o.textContent=String(n.index+1).padStart(2,` `);let s=document.createElement(`span`);s.className=`name`,s.textContent=n.name;let c=document.createElement(`span`);c.className=`time`,c.textContent=n.seconds>0?e(n.seconds):`--:--`,a.append(o,s,c),t.push(a)}n.playlist.replaceChildren(...t)}let h=b(),g=T(),_;for(let e of Array.from(n.playlist.children)){let t=e,n=Number(t.dataset.index),r=Number.isInteger(n)&&n===h;t.classList.toggle(`selected`,r),t.classList.toggle(`playing`,r&&g),r&&(_=t)}h!==ve&&(ve=h,_?.scrollIntoView({block:`nearest`}))}function xe(e){let t=document.createElement(`li`);t.className=`group`;let r=document.createElement(`span`);if(r.className=`group-name`,r.textContent=e===``?`This server's library`:e,t.append(r),e!==``&&!n.adminPanel.hidden){let n=document.createElement(`button`);n.type=`button`,n.className=`group-remove`,n.textContent=`×`,n.title=`Remove ${e} from the playlist`,n.setAttribute(`aria-label`,`Remove ${e} from the playlist`),n.addEventListener(`click`,t=>{t.stopPropagation(),Se(e)}),t.append(n)}return t}async function Se(e){try{let t=await fetch(v.url(`/api/source/remove`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({group:e})}),n=await t.json();H(t.ok?`Removed ${n.removed??0} tracks from ${e}.`:n.error??`that did not work`)}catch{H(`could not reach the server`)}}function Ce(){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(g())m=M(m,p);else{let e=_.read();e.length>0&&(h.length!==25&&(h=de(24,e.length)),p=pe(p,fe(e,h)),m=M(m,p))}if(s){let e=getComputedStyle(document.documentElement);me(s,{width:t.width,height:t.height},p,m,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(T()){n.glyphs.textContent=p.map(P).join(``);let[t,r]=g()?o.levels:_.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(w());let i=C();!u&&i>0&&(n.seek.value=String(Math.round(w()/i*1e3)))}requestAnimationFrame(Ce)}function z(e){n.video.hidden=!e,n.fullscreen.hidden=!e}function we(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:x(),album:S(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void O()),navigator.mediaSession.setActionHandler(`pause`,()=>void O()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void A(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void A(-1)))}n.filter.addEventListener(`input`,()=>{I=``,R()}),n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&E(n)}),n.fullscreen.addEventListener(`click`,()=>{let e=n.video;if(document.fullscreenElement){document.exitFullscreen().catch(()=>{});return}if(typeof e.requestFullscreen==`function`){e.requestFullscreen().catch(()=>{e.webkitEnterFullscreen?.()});return}e.webkitEnterFullscreen?.()}),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 O()),n.seek.addEventListener(`input`,()=>{u=!0}),n.seek.addEventListener(`change`,()=>{let e=C();e>0&&_.seek(Number(n.seek.value)/1e3*e),u=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;_.volume=e;try{localStorage.setItem(ge,String(e))}catch{}});let Te=e=>{e.addEventListener(`change`,()=>{let t=ee(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,F();return}te(i),i=t,a=0,r=`local`,v.close(),l=``,E(0)})};Te(n.files),Te(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.remoteUrl.value,{base:i,key:a}=ae(t);if(i===``){l=`That is not an address.`,F();return}(async()=>{s=`connecting`,F();let e=ue(i);if(e){s=`error`,c=e,l=e,r=`local`,F();return}if(await k(i,void 0,a)===null){s=`error`;let e=ce(i);c=e?`needs the server's name`:`not answering`,l=e||`Nothing answered at ${i}. If that is your machine, it is off or nixamp is not running on it; otherwise check the address.`,r=`local`,F();return}let n=await le(i,a);if(n){s=`error`,c=n,l=n,r=`local`,F();return}r=`remote`,l=``;try{localStorage.setItem(he,t.trim())}catch{}v.connect(t),X(),qe(!0),U(),J(),F()})()});let B=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);if(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&&K&&t.ownerId!==K&&e.append(Le(t.ownerId,t.name)),t.ownerId&&K&&t.ownerId===K){let r=document.createElement(`button`);r.type=`button`,r.className=`ghost`,r.textContent=`Take off the list`,r.addEventListener(`click`,e=>{e.stopPropagation(),r.disabled=!0,(async()=>{try{let e=await fetch(`/api/directory?id=${encodeURIComponent(t.id)}`,{method:`DELETE`}),r=await e.json().catch(()=>({}));n.directoryNote.textContent=e.ok?`${t.name} is off the list.`:r.error??`that did not work`}catch{n.directoryNote.textContent=`could not reach the directory`}finally{await B()}})()}),e.append(r)}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),B()}let V=null,Ee=e=>e===`cgnat`?`mobile or tailscale`:e===`private`?`your network`:e===`local`?`this machine`:e===`public`?`the internet`:e,De=e=>e===`events`?`watching the panel`:e===`page`?`opened the page`:e===`media`?`playing a track`:e===`stream`?`listening live`:e,Oe=``,ke=``,Ae=e=>{let t=e.map(e=>`${e.address}|${e.kind}|${e.track}|${Math.round(e.bytes/4096)}|${e.endedAt}`).join(`~`);if(t===Oe)return;Oe=t,n.adminConnections.replaceChildren();let r=document.createElement(`tr`);for(let e of[`Where`,`Network`,`Kind`,`Client`,`Track`,`Sent`]){let t=document.createElement(`th`);t.textContent=e,r.append(t)}n.adminConnections.append(r);for(let t of e.slice(0,40)){let e=document.createElement(`tr`);t.endedAt!==null&&(e.className=`ended`);let r=[[t.address,``],[Ee(t.network),`network-${t.network}`],[De(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)}};function H(e){n.adminSaid.textContent=e,n.adminSaid.hidden=e===``}let je=async()=>{try{let e=await fetch(v.url(`/api/connections`));if(!e.ok)return;let t=await e.json(),r=(t.connections??[]).filter(e=>e.endedAt===null&&e.kind!==`media`&&e.kind!==`stream`).length,i=t.active??0;n.adminNote.textContent=r===0?`${i} listening now.`:`${i} listening now, and ${r} with the page open.`,Ae(t.connections??[]),Me(t.publish??[],(t.channels??[]).map(e=>e.id)),Je(t.home??``,t.root??``),X()}catch{n.adminNote.textContent=`lost touch with the server`}};function Me(e,t){n.publishPanel.hidden=e.length===0;let r=`${e.map(e=>`${e.id}=${e.url}`).join(`~`)}::${t.join(`,`)}`;if(r===ke)return;if(ke=r,e.length===0){n.publishList.replaceChildren();return}let i=e.length-t.length;n.publishNote.textContent=`Point OBS, Larix or ffmpeg at one of these. One publisher per URL — ${e.length} at once, ${i} free right now.`,n.publishList.replaceChildren(...e.map(e=>{let n=t.includes(e.id),r=document.createElement(`li`);n&&(r.className=`in-use`);let i=document.createElement(`span`);i.className=`slot`,i.textContent=n?`${e.id} · live`:e.id;let a=document.createElement(`input`);a.type=`text`,a.readOnly=!0,a.value=e.url,a.setAttribute(`aria-label`,`RTMP URL for ${e.id}`);let o=document.createElement(`button`);return o.type=`button`,o.className=`ghost`,o.textContent=`Copy`,o.addEventListener(`click`,()=>{a.select(),navigator.clipboard?.writeText(e.url).catch(()=>{})}),r.append(i,a,o),r}))}let U=async()=>{if(r!==`remote`){n.adminPanel.hidden=!0,n.publishPanel.hidden=!0,V&&clearInterval(V),V=null;return}let e=!1,t=null,i=!1;try{let n=await fetch(v.url(`/api/admin`));if(n.ok){let r=await n.json();e=r.allowed===!0,t=r.as??null,i=r.claimed===!0}}catch{e=!1}if(n.adminPanel.hidden=!e,V&&clearInterval(V),V=null,J(),n.listenOnly.hidden=e,!e){n.listenOnly.textContent=i?`This is a listen-only link: you can hear this server but not change what it plays. Use its control link — the first one it printed — or sign in as its owner.`:`This is a listen-only link: you can hear this server but not change what it plays. Use its control link, the first one it printed.`;return}n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,je(),V=setInterval(()=>void je(),2e3)};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();if(!t)return;H(`Reading ${t}…`);let r=n.adminReplace.checked;(async()=>{try{let e=await fetch(v.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...r?{replace:!0}:{}})}),i=await e.json();H(e.ok?r?`Now serving ${t}.`:i.added===0?`Everything there was already in the playlist.`:`Added ${i.added??0} tracks from ${t}.`:i.error??`that did not work`),e.ok&&(n.adminSource.value=``,J(),X())}catch{H(`could not reach the server`)}})()});let Ne=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=K?e.filter(e=>e.ownerId&&e.ownerId!==K):[];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 ${Ne(e.endedAt)}`:`ended ${Ne(e.endedAt)}`,r.append(i,a),t.append(r,Le(e.ownerId,e.name)),n.recentList.append(t)}},Fe=async()=>{n.serversList.replaceChildren();try{let e=await fetch(`/api/v1/servers`);if(!e.ok){n.serversPanel.hidden=!0;return}let t=(await e.json()).servers??[];n.serversPanel.hidden=!1,n.serversNote.textContent=t.length===0?"No servers yet. `nixamp server add --here` remembers the one you are running.":`The machines on your account. Open one, or forget it.`;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.url,r.append(i,a);let o=document.createElement(`button`);o.type=`button`,o.className=`button`,o.textContent=`Open`,o.addEventListener(`click`,()=>{n.remoteUrl.value=e.key?`${e.url}/admin/${e.key}`:e.url,n.remoteForm.requestSubmit()}),k(e.url).then(n=>{if(n!==null){a.textContent=`${e.url} · ${n}`;return}a.textContent=`${e.url} · not answering`,t.classList.add(`offline`),o.disabled=!0,o.title=`That machine is not answering. Start nixamp on it.`});let s=document.createElement(`button`);s.type=`button`,s.className=`ghost`,s.textContent=`Forget`,s.addEventListener(`click`,()=>{(async()=>{s.disabled=!0;try{await fetch(`/api/v1/servers/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await Fe()}catch{s.disabled=!1}})()}),t.append(r,o,s),n.serversList.append(t)}}catch{n.serversPanel.hidden=!0}},Ie=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}},Le=(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),Ie())}catch{}finally{n.disabled=!1}})()}),n},Re=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},ze=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,Be=async()=>{if(!ze())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:Re(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{}},W=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`}},He=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=ze()&&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 Be();n.notifyWeb.checked=e,await W({wantsWeb:e});return}await Ve(),await W({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{W({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 W({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),W({phone:n.notifyPhone.value.trim()})});let G=!1,K=``,q=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(He(),Ie(),Fe()):(n.serversPanel.hidden=!0,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}.`:G?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=G?`Create account`:`Sign in`,n.accountToggle.textContent=G?`I have one`:`Create one`,n.accountPassword.autocomplete=G?`new-password`:`current-password`},Ue=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)}},We=async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();K=e.ok?t.account?.id??``:``,q(e.ok?t.account?.email??`you`:null)}catch{K=``,q(null)}Ge()};function Ge(){if(f===``)return;if(K===``){n.accountNote.textContent=`Sign in to watch the stream you were sent.`,n.accountPanel.scrollIntoView({behavior:`smooth`,block:`center`});return}let e=f;f=``,n.remoteUrl.value=e,n.remoteForm.requestSubmit()}n.accountToggle.addEventListener(`click`,()=>{G=!G,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/${G?`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}K=i.account?.id??``,n.accountPassword.value=``,q(i.account?.email??t),U(),Ge()}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{}K=``,q(null),U()})()});try{let e=new URL(globalThis.location.href).searchParams.get(`url`)??``;e!==``&&(f=e,n.remoteUrl.value=e,l=`Sign in to watch this stream.`,globalThis.history?.replaceState(null,``,globalThis.location.pathname))}catch{}Ue(),We(),U(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}B(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{v.close(),n.listenOnly.hidden=!0,d=-1,n.sharePanel.hidden=!0,n.publishPanel.hidden=!0,n.adminPanel.hidden=!0,n.onairPanel.hidden=!0,qe(!1),r=`local`,s=`idle`,c=``,F()});async function J(){if(r!==`remote`||v.shareLink===``){n.sharePanel.hidden=!0;return}n.sharePanel.hidden=!1;let e=v.shareLink,t=globalThis.location.origin;n.shareLink.value=e.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e)}`:e,n.shareNote.textContent=`Anyone with this link can watch. They sign in once, then it opens.`,n.sharePhone.hidden=!0,n.shareSend.hidden=!0,n.liveControls.hidden=!0;let i=``;try{let e=await fetch(`/api/directory`);e.ok&&(i=(await e.json()).callIn??``)}catch{}let a=null;try{let r=await fetch(v.url(`/api/live/state`));r.ok&&(a=await r.json()),a?.url&&(e=a.url,n.shareLink.value=e.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e)}`:e)}catch{}if(a){if(n.liveControls.hidden=n.adminPanel.hidden||!a.possible,n.goLive.hidden=a.live,n.stopLive.hidden=!a.live,n.sharePhone.hidden=!1,!a.live){n.sharePhone.textContent=a.possible?`Not listed, so nobody can find this in the directory. Go live to list it, with a phone number and a code anyone can call.`:`This machine has no address the world can reach, so it cannot be listed.`;return}if(!i){n.sharePhone.textContent=`Listed. The code for the phone line is ${a.code}.`,n.shareSend.hidden=!1;return}n.sharePhone.replaceChildren(document.createTextNode(`To talk about it, call `),Xe(i),document.createTextNode(` and key `),Xe(a.code),document.createTextNode(`. That is a room with everyone else watching — not the stream itself.`)),n.shareSend.hidden=!1}}let Ke=``,Y=null,qe=e=>{Y&&clearInterval(Y),Y=null,e&&(Y=setInterval(()=>void X(),6e3))};async function X(){if(r!==`remote`){n.onairPanel.hidden=!0;return}let e;try{let t=await fetch(v.url(`/api/streams`));if(!t.ok){n.onairPanel.hidden=!0;return}e=await t.json()}catch{n.onairPanel.hidden=!0;return}n.onairPanel.hidden=!1;let t=JSON.stringify(e);if(t===Ke)return;Ke=t;let i=e.restreams??[],a=e.channels.length+i.length;n.onairNote.textContent=a===0?`One stream, from this server's own files.`:`${a+1} streams: this server's own files, and ${a} more on it.`;let o=[];o.push(Z({title:e.server.name,detail:[e.server.nowPlaying||`nothing loaded`,`${e.server.tracks} track${e.server.tracks===1?``:`s`}`,e.server.live&&e.server.code?`☎ ${e.server.code}`:`not listed`].join(` · `),onPlay:()=>{E(b())},link:e.server.live?e.server.url:``}));for(let e of i)o.push(Z({title:e.name,detail:e.tracks===1?`re-streamed from the web`:`re-streamed from the web · ${e.tracks} tracks`,onPlay:()=>{E(e.at)},link:``}));for(let t of e.channels)o.push(Z({title:t.name,detail:`live over ${t.via} · ${t.listeners} listening`,onPlay:()=>{_.load({title:t.name,artist:``,album:``,duration:0,url:v.url(`/api/channels/${encodeURIComponent(t.id)}`),video:!0,objectUrl:!1},!0),z(!0)},link:``}));n.onairList.replaceChildren(...o)}function Z(e){let t=document.createElement(`li`),n=document.createElement(`span`);n.className=`recent-label`;let r=document.createElement(`span`);r.className=`name`,r.textContent=e.title;let i=document.createElement(`span`);i.className=`detail`,i.textContent=e.detail,n.append(r,i);let a=document.createElement(`button`);if(a.type=`button`,a.className=`button`,a.textContent=`Play`,a.addEventListener(`click`,e.onPlay),t.append(n,a),e.link){let n=document.createElement(`button`);n.type=`button`,n.className=`ghost`,n.textContent=`Copy link`,n.addEventListener(`click`,()=>{let t=globalThis.location.origin,n=e.link.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e.link)}`:e.link;navigator.clipboard?.writeText(n).catch(()=>{})}),t.append(n)}return t}let Q=``;function Je(e,t){Q=e;let r=e!==``&&t===e;n.loadHome.hidden=e===``,n.homeNote.hidden=e===``,e!==``&&(n.homeNote.textContent=r?`This server's own files: ${e}`:`This server's own files are ${e}, and are not in the playlist.`,n.loadHome.disabled=!1)}n.loadHome.addEventListener(`click`,()=>{Q!==``&&(n.loadHome.disabled=!0,H(`Reading this server's files…`),(async()=>{try{let e=await fetch(v.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:Q})}),t=await e.json();H(e.ok?t.added===0?`This server's files are already in the playlist.`:`Loaded ${t.added??0} of this server's own files.`:t.error??`that did not work`)}catch{H(`could not reach the server`)}finally{n.loadHome.disabled=!1}})())});let Ye=async e=>{n.goLive.disabled=!0,n.stopLive.disabled=!0,n.shareNote.textContent=e?`Going live…`:`Taking it off the list…`;try{let t=await fetch(v.url(e?`/api/live/start`:`/api/live/stop`),{method:`POST`}),r=await t.json();n.shareNote.textContent=t.ok?e?`Live. Anyone can call and key ${r.code??``} to talk about it.`:`Taken off the list. The link still works for anybody who has it.`:r.error??`that did not work`}catch{n.shareNote.textContent=`could not reach the server`}finally{n.goLive.disabled=!1,n.stopLive.disabled=!1,await J()}};n.goLive.addEventListener(`click`,()=>void Ye(!0)),n.stopLive.addEventListener(`click`,()=>void Ye(!1));function Xe(e){let t=document.createElement(`b`);return t.textContent=e,t}n.shareCopy.addEventListener(`click`,()=>{n.shareLink.select(),navigator.clipboard?.writeText(n.shareLink.value).then(()=>{n.shareNote.textContent=`Copied. Send it to anybody.`},()=>{n.shareNote.textContent=`Copy it from the box above.`})}),n.shareSend.addEventListener(`submit`,e=>{e.preventDefault();let t=n.shareTo.value.trim();t!==``&&(async()=>{n.shareNote.textContent=`Sending…`;try{let e=await fetch(`/api/v1/invite`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({to:t,stream:v.shareLink})}),r=await e.json();n.shareNote.textContent=e.ok?`Sent to ${r.sent??t}.`:r.error??`that did not send`,e.ok&&(n.shareTo.value=``)}catch{n.shareNote.textContent=`could not send that`}})()}),n.listenHere.addEventListener(`change`,()=>{try{localStorage.setItem(_e,n.listenHere.checked?`1`:`0`)}catch{}r===`remote`&&(async()=>{n.listenHere.checked?(await v.send({type:`stop`}),await D(o.index)):(_.stop(),d=-1),F()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),O();return;case`s`:j();return;case`n`:case`ArrowRight`:A(1);return;case`p`:case`ArrowLeft`:A(-1);return;case`ArrowDown`:e.preventDefault(),E(Math.min(y()-1,b()+1));return;case`ArrowUp`:e.preventDefault(),E(Math.max(0,b()-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(ge);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),_.volume=Number(e));let t=localStorage.getItem(he);t&&(n.remoteUrl.value=t),localStorage.getItem(_e)===`0`&&(n.listenHere.checked=!1)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await k(e)===null)return;let t=await se(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,r=`remote`,l=``,v.connect(e),F())})(),F(),requestAnimationFrame(Ce)}P(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
@@ -0,0 +1 @@
1
+ :root{--bg:#080c09;--panel:#0c120e;--edge:#1d2c22;--green:#4af689;--green-dim:#227a4a;--fg:#cfe8d8;--muted:#6d8a79;--warn:#e8c35a;--accent:#7ef0c4;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}[hidden]{display:none!important}html,body{background:var(--bg);min-height:100%;color:var(--fg);margin:0;font:14px/1.45 ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace}body{background-image:repeating-linear-gradient(#4af68906 0 1px,#0000 1px 3px)}#app{max-width:1100px;padding:calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));flex-direction:column;gap:10px;margin:0 auto;display:flex}.bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.brand{color:var(--green);letter-spacing:.14em;font-weight:700}.status{color:var(--muted)}.status[data-playing=true]{color:var(--green)}.chip{color:var(--muted);border:1px solid var(--edge);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:46vw;margin-left:auto;padding:1px 10px;font-size:12px;overflow:hidden}.panel{border:1px solid var(--edge);background:var(--panel);border-radius:6px;min-width:0;padding:14px 12px 12px;position:relative}.panel:before{content:attr(data-title);background:var(--panel);color:var(--green-dim);letter-spacing:.06em;padding:0 6px;font-size:12px;position:absolute;top:-.72em;left:10px}.split{grid-template-columns:1.3fr 1fr;gap:10px;display:grid}.split>.col-a{grid-column:1}.split>.col-b{grid-column:2}@media (max-width:720px){.split{grid-template-columns:1fr}.split>.col-a,.split>.col-b{grid-column:auto}}.track-title{color:var(--accent);text-overflow:ellipsis;white-space:nowrap;font-weight:700;overflow:hidden}.track-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}#video{border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px}.scrub{align-items:center;gap:10px;margin-top:6px;display:flex}.time{color:var(--fg);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}input[type=range]{appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:14px}input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(var(--edge), var(--edge));border:1px solid var(--edge);border-radius:3px;height:6px}input[type=range]::-moz-range-track{background:var(--edge);border-radius:3px;height:6px}input[type=range]::-webkit-slider-thumb{appearance:none;background:var(--green);border-radius:2px;width:10px;height:16px;margin-top:-6px}input[type=range]::-moz-range-thumb{background:var(--green);border:0;border-radius:2px;width:10px;height:16px}input[type=range]:disabled{opacity:.45;cursor:default}#spectrum{border:1px solid var(--edge);background:#060a07;border-radius:4px;width:100%;height:190px;display:block}.meters{align-items:baseline;gap:10px;margin-top:6px;display:flex;overflow:hidden}.glyphs{color:var(--green);letter-spacing:1px;white-space:nowrap;min-height:1.4em;overflow:hidden}.levelmeter{color:var(--accent);white-space:nowrap;margin-left:auto}.filter{border:1px solid var(--edge);width:100%;color:var(--fg);font:inherit;background:#0a100c;border-radius:4px;margin-bottom:6px;padding:4px 8px}.filter:focus{border-color:var(--accent);outline:none}.crumbs{color:var(--muted);flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:6px;font-size:12px;display:flex}.crumbs button{font:inherit;color:var(--accent);cursor:pointer;background:0 0;border:0;padding:0 2px}.crumbs button:hover{text-decoration:underline}.crumbs .here{color:var(--fg)}.folder{cursor:pointer;white-space:nowrap;color:var(--accent);border-radius:3px;gap:8px;padding:2px 6px;display:flex}.folder:hover{background:#142019}.folder .name{text-overflow:ellipsis;flex:1;overflow:hidden}.folder .count{color:var(--muted);flex:none}.playlist{scrollbar-color:var(--green-dim) transparent;max-height:214px;margin:0;padding:0;list-style:none;overflow-y:auto}.row{cursor:pointer;white-space:nowrap;border-radius:3px;gap:8px;padding:2px 6px;display:flex}.row:hover{background:#142019}.row.selected{background:#16241c}.row.selected .name{color:var(--accent)}.row.playing .name{color:var(--green)}.group{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;border-top:1px solid var(--edge);align-items:center;gap:8px;margin-top:4px;padding:6px 6px 2px;font-size:11px;display:flex}.group:first-child{border-top:0;margin-top:0}.group-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.group-remove{border:1px solid var(--edge);color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;line-height:1.4}.group-remove:hover{color:var(--accent);border-color:var(--accent)}.row .n{color:var(--muted);text-align:right;flex:none;width:2.4em}.row .name{text-overflow:ellipsis;flex:1;overflow:hidden}.row .time{color:var(--muted);flex:none}.transport{border:1px solid var(--edge);background:var(--panel);border-radius:6px;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 10px;display:flex}button{font:inherit;color:var(--fg);border:1px solid var(--edge);cursor:pointer;background:#121a15;border-radius:4px;padding:6px 12px}button:hover{border-color:var(--green-dim);color:var(--green)}button:active{transform:translateY(1px)}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid var(--green);outline-offset:2px}button.primary{color:var(--green);border-color:var(--green-dim);min-width:64px}button.ghost{color:var(--muted);background:0 0}.volume{flex:1;align-items:center;gap:8px;min-width:120px;max-width:220px;margin-left:auto;display:flex}.vol{color:var(--muted);letter-spacing:.08em;font-size:12px}.picker{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.button{border:1px solid var(--edge);cursor:pointer;color:var(--fg);background:#121a15;border-radius:4px;padding:6px 12px;display:inline-block}.button:hover{border-color:var(--green-dim);color:var(--green)}.button input[type=file]{display:none}#remote-url{font:inherit;min-width:12ch;color:var(--fg);border:1px solid var(--edge);background:#060a07;border-radius:4px;flex:1;padding:6px 10px}.check{color:var(--muted);align-items:center;gap:8px;margin-top:10px;display:flex}.check input{accent-color:var(--green)}.hint{color:var(--muted);margin:0 0 8px;font-size:13px}.hint code{color:var(--accent)}#remote-state[data-status=live]{color:var(--green)}#remote-state[data-status=connecting],#remote-state[data-status=error]{color:var(--warn)}.note{color:var(--warn);border-left:2px solid var(--warn);margin:0;padding-left:8px}.statusbar{color:var(--muted);border-top:1px solid var(--edge);flex-wrap:wrap;gap:14px;padding-top:8px;font-size:12px;display:flex}.statusbar b{color:var(--green);font-weight:700}.statusbar .spacer{flex:1}.statusbar a{color:var(--muted)}.directory{border-top:1px solid var(--line);margin-top:.6rem;padding-top:.6rem}.directory-list{max-height:12rem;margin:0;padding:0;list-style:none;overflow-y:auto}.directory-list li+li{margin-top:.3rem}.directory-list button{border:1px solid var(--line);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:4px;padding:.4rem .5rem}.directory-list button:hover,.directory-list button:focus-visible{border-color:var(--accent);background:#ffffff0a}.directory-list .name{color:var(--accent);display:block}.directory-list .detail{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:.85em;display:block;overflow:hidden}.publish-list{margin:.3rem 0 0;padding:0;list-style:none}.publish-list li{align-items:center;gap:8px;margin-top:.3rem;display:flex}.publish-list li.in-use .slot{color:var(--green)}.publish-list .slot{color:var(--muted);flex:none;min-width:4.5em;font-size:12px}.publish-list input{flex:auto;min-width:0}.said{color:var(--accent);margin:.3rem 0 0}.directory-list li.offline .recent-label{opacity:.55}.directory-list li.offline .detail{color:var(--warn)}.share-line{align-items:center;gap:8px;margin:.4rem 0;display:flex}.share-what{color:var(--muted);letter-spacing:.06em;text-transform:uppercase;flex:none;font-size:12px}.share-line input{flex:auto;min-width:0}#share-phone{color:var(--fg);font-size:1.05em}#share-phone b{color:var(--accent)}.admin-table{border-collapse:collapse;width:100%;min-height:5.5em;max-height:14rem;margin:.4rem 0;font-size:.85em;display:block;overflow-y:auto}.admin-table th{text-align:left;opacity:.6;padding:.2rem .4rem .2rem 0;font-weight:400}.admin-table td{white-space:nowrap;text-overflow:ellipsis;max-width:12rem;padding:.2rem .4rem .2rem 0;overflow:hidden}.admin-table td.network-public{color:var(--warning,#e0b341)}.admin-table td.network-private{color:var(--success,#7fd18b)}.admin-table tr.ended{opacity:.45}body.route-directory .player-only{display:none}.directory-list li{align-items:stretch;gap:.4rem;display:flex}.directory-list li>:first-child{flex:auto;min-width:0}.directory-list li>button:first-child{text-align:left;width:100%}.directory-list li>.button,.directory-list li>.ghost{white-space:nowrap;flex:none;align-items:center;display:flex}.directory-list .follow{white-space:nowrap;width:auto;color:var(--muted);flex:none;padding-inline:.6rem}.directory-list .follow[data-following=yes]{border-color:var(--accent);color:var(--accent)}.toggle{color:var(--muted);cursor:pointer;align-items:center;gap:.35rem;display:inline-flex}.toggle input{accent-color:var(--accent)}.recent-label{border:1px solid var(--line);border-radius:4px;flex:auto;min-width:0;padding:.4rem .5rem}