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.
- package/dist/protocol.d.ts +10 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +54 -4
- package/package.json +1 -1
- package/src/protocol.ts +10 -0
- package/src/server.ts +66 -4
- package/web/dist/assets/{hls-3VKVEQE3-C2NU6wnG.js → hls-3VKVEQE3-CYp2nKVZ.js} +1 -1
- package/web/dist/assets/index-CuPGZuSo.js +1 -0
- package/web/dist/assets/index-DDa16PVk.css +1 -0
- package/web/dist/assets/{mpegts-DDXequBN.js → mpegts-BhdFS9RT.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-h1JlF4Fz.js → mpegts-LO6RVLD6-SQvnAUEE.js} +1 -1
- package/web/dist/index.html +7 -2
- package/web/dist/sw.js +6 -6
- package/web/dist/assets/index-BjUE4krU.js +0 -1
- package/web/dist/assets/index-OC1WbK88.css +0 -1
package/dist/protocol.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
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
|
|
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 {
|
|
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-
|
|
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}
|