nixamp 0.7.32 → 0.7.34
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 +11 -0
- package/dist/server.d.ts +26 -2
- package/dist/server.js +51 -8
- package/package.json +1 -1
- package/src/protocol.ts +11 -0
- package/src/server.ts +66 -10
- package/web/dist/assets/{hls-3VKVEQE3-CHhX3iNj.js → hls-3VKVEQE3-ZK5W33OV.js} +1 -1
- package/web/dist/assets/index-DFWY01QV.js +1 -0
- package/web/dist/assets/{mpegts-BgCSJS3e.js → mpegts--5mHOXme.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CvAT8Z_G.js → mpegts-LO6RVLD6-DWE7mc-Q.js} +1 -1
- package/web/dist/index.html +5 -3
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-DrIVrMnV.js +0 -1
package/dist/protocol.d.ts
CHANGED
|
@@ -38,6 +38,17 @@ export interface RemoteTrack {
|
|
|
38
38
|
* somebody's disk is their business.
|
|
39
39
|
*/
|
|
40
40
|
folder?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Whether this comes off the network rather than off a disk.
|
|
43
|
+
*
|
|
44
|
+
* A channel and a film in a folder are different kinds of thing and belong
|
|
45
|
+
* in different lists -- one is live and one is on demand. Which it is has to
|
|
46
|
+
* be a property of the track, because it was being inferred from how it
|
|
47
|
+
* happened to be loaded: a re-stream that replaced the playlist carried no
|
|
48
|
+
* group, so it appeared in no list of live streams at all and there was
|
|
49
|
+
* nowhere to click it.
|
|
50
|
+
*/
|
|
51
|
+
remote?: boolean;
|
|
41
52
|
}
|
|
42
53
|
/** Everything a remote needs to draw the player. */
|
|
43
54
|
export interface Snapshot {
|
package/dist/server.d.ts
CHANGED
|
@@ -150,6 +150,15 @@ export type Loaded = Track & {
|
|
|
150
150
|
* the source is added, rather than guessed from a URL that has no opinion.
|
|
151
151
|
*/
|
|
152
152
|
picture?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Whether a person named this, rather than a tagger.
|
|
155
|
+
*
|
|
156
|
+
* Tags are read in the background and merged in when they arrive, which is
|
|
157
|
+
* right for a library and wrong for a channel: somebody called it "MLB
|
|
158
|
+
* Network", ffprobe came back a moment later with "932", and the name they
|
|
159
|
+
* chose vanished on its own.
|
|
160
|
+
*/
|
|
161
|
+
named?: boolean;
|
|
153
162
|
};
|
|
154
163
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
155
164
|
export interface Engine {
|
|
@@ -184,8 +193,11 @@ export interface Engine {
|
|
|
184
193
|
* What somebody means by putting a folder in a box: the album shows up at
|
|
185
194
|
* the bottom of the playlist under its own name, and the music that was
|
|
186
195
|
* already there is still there. Answers how many tracks were new.
|
|
196
|
+
*
|
|
197
|
+
* `called` is what a person named it, which beats whatever the address ends
|
|
198
|
+
* in -- a channel is "MLB Network", not "932".
|
|
187
199
|
*/
|
|
188
|
-
add(tracks: Track[], from: string): number;
|
|
200
|
+
add(tracks: Track[], from: string, called?: string): number;
|
|
189
201
|
/**
|
|
190
202
|
* Take an added source back out again, by the name `add` gave it.
|
|
191
203
|
*
|
|
@@ -288,7 +300,7 @@ export declare class PlayerEngine implements Engine {
|
|
|
288
300
|
* Paths already loaded are skipped, so adding the same album twice is not
|
|
289
301
|
* two copies of it.
|
|
290
302
|
*/
|
|
291
|
-
add(tracks: Track[], from: string): number;
|
|
303
|
+
add(tracks: Track[], from: string, called?: string): number;
|
|
292
304
|
/**
|
|
293
305
|
* Take an added source back out.
|
|
294
306
|
*
|
|
@@ -317,6 +329,18 @@ export declare class PlayerEngine implements Engine {
|
|
|
317
329
|
fill(tracks: Loaded[], root: string): void;
|
|
318
330
|
retag(tracks: Track[], root: string): void;
|
|
319
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* The streams a server is carrying, gathered by name.
|
|
334
|
+
*
|
|
335
|
+
* A whole re-streamed folder is one entry rather than twenty: somebody
|
|
336
|
+
* looking at what is on wants "that album from the web", not every track in
|
|
337
|
+
* it. An entry names where to start, so clicking it plays.
|
|
338
|
+
*/
|
|
339
|
+
export declare function liveOnes(engine: Engine): {
|
|
340
|
+
name: string;
|
|
341
|
+
at: number;
|
|
342
|
+
tracks: number;
|
|
343
|
+
}[];
|
|
320
344
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
321
345
|
export declare class EmptyEngine implements Engine {
|
|
322
346
|
private readonly note;
|
package/dist/server.js
CHANGED
|
@@ -308,6 +308,9 @@ export function toRemoteTracks(tracks) {
|
|
|
308
308
|
// how a client knows they are the library.
|
|
309
309
|
...(t.group ? { group: t.group } : {}),
|
|
310
310
|
...(t.folder ? { folder: t.folder } : {}),
|
|
311
|
+
// Said plainly rather than guessed at from how it was loaded: this is
|
|
312
|
+
// what puts a channel in the live list and a film in the library.
|
|
313
|
+
...(isRemote(t.path) ? { remote: true } : {}),
|
|
311
314
|
}));
|
|
312
315
|
}
|
|
313
316
|
/** Video containers, as opposed to the songs that are most of a library. */
|
|
@@ -575,8 +578,12 @@ export class PlayerEngine {
|
|
|
575
578
|
* Paths already loaded are skipped, so adding the same album twice is not
|
|
576
579
|
* two copies of it.
|
|
577
580
|
*/
|
|
578
|
-
add(tracks, from) {
|
|
579
|
-
|
|
581
|
+
add(tracks, from, called = "") {
|
|
582
|
+
// What somebody called it beats what the address happens to end in. A
|
|
583
|
+
// channel at .../932 is "932" to a URL and "MLB Network" to a person, and
|
|
584
|
+
// no transport stream reliably says which -- the one in front of us calls
|
|
585
|
+
// itself "Service01".
|
|
586
|
+
const group = called || sourceLabel(from);
|
|
580
587
|
const known = new Set(this.tracks.map((track) => track.path));
|
|
581
588
|
const fresh = tracks
|
|
582
589
|
.filter((track) => !known.has(track.path))
|
|
@@ -679,6 +686,9 @@ export class PlayerEngine {
|
|
|
679
686
|
...tagged,
|
|
680
687
|
...(track.group ? { group: track.group } : {}),
|
|
681
688
|
...(track.folder ? { folder: track.folder } : {}),
|
|
689
|
+
// A name somebody chose outlives whatever the stream says about
|
|
690
|
+
// itself. It called itself "932".
|
|
691
|
+
...(track.named ? { title: track.title, named: true } : {}),
|
|
682
692
|
};
|
|
683
693
|
});
|
|
684
694
|
if (!changed)
|
|
@@ -690,6 +700,28 @@ export class PlayerEngine {
|
|
|
690
700
|
this.push(true);
|
|
691
701
|
}
|
|
692
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* The streams a server is carrying, gathered by name.
|
|
705
|
+
*
|
|
706
|
+
* A whole re-streamed folder is one entry rather than twenty: somebody
|
|
707
|
+
* looking at what is on wants "that album from the web", not every track in
|
|
708
|
+
* it. An entry names where to start, so clicking it plays.
|
|
709
|
+
*/
|
|
710
|
+
export function liveOnes(engine) {
|
|
711
|
+
const tracks = engine.snapshot().tracks ?? [];
|
|
712
|
+
const found = new Map();
|
|
713
|
+
tracks.forEach((track, at) => {
|
|
714
|
+
if (track.remote !== true)
|
|
715
|
+
return;
|
|
716
|
+
const name = track.group || track.title;
|
|
717
|
+
const already = found.get(name);
|
|
718
|
+
if (already)
|
|
719
|
+
already.tracks += 1;
|
|
720
|
+
else
|
|
721
|
+
found.set(name, { name, at, tracks: 1 });
|
|
722
|
+
});
|
|
723
|
+
return [...found.values()];
|
|
724
|
+
}
|
|
693
725
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
694
726
|
export class EmptyEngine {
|
|
695
727
|
note;
|
|
@@ -1748,11 +1780,14 @@ export function createHandler(engine, options) {
|
|
|
1748
1780
|
// what made moving between a channel and an album so confusing. Named
|
|
1749
1781
|
// here with the first track it owns, so it can be played from the list
|
|
1750
1782
|
// of what is live rather than hunted for among five thousand files.
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1783
|
+
// Every stream coming off the network, however it got here.
|
|
1784
|
+
//
|
|
1785
|
+
// This used to read the groups, which only exist for sources that were
|
|
1786
|
+
// added -- so re-streaming something as a replacement produced a
|
|
1787
|
+
// channel that appeared in no list anywhere, with nothing to click.
|
|
1788
|
+
// What makes a thing live is where it comes from, not which button
|
|
1789
|
+
// loaded it.
|
|
1790
|
+
restreams: liveOnes(engine),
|
|
1756
1791
|
});
|
|
1757
1792
|
return;
|
|
1758
1793
|
}
|
|
@@ -2089,10 +2124,13 @@ export function createHandler(engine, options) {
|
|
|
2089
2124
|
return;
|
|
2090
2125
|
}
|
|
2091
2126
|
let source = "";
|
|
2127
|
+
let called = "";
|
|
2092
2128
|
let replacing = false;
|
|
2093
2129
|
try {
|
|
2094
2130
|
const body = JSON.parse(await readBody(request));
|
|
2095
2131
|
source = String(body.source ?? "");
|
|
2132
|
+
// Trimmed and capped: it is a label, and it can end up in a listing.
|
|
2133
|
+
called = String(body.name ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 80);
|
|
2096
2134
|
// Adding is what somebody means by putting a folder in a box, so it is
|
|
2097
2135
|
// the default. Replacing is the much larger claim that this server now
|
|
2098
2136
|
// serves that instead, so it is the one you have to ask for.
|
|
@@ -2124,12 +2162,17 @@ export function createHandler(engine, options) {
|
|
|
2124
2162
|
return codecs.video === "" ? track : { ...track, picture: true };
|
|
2125
2163
|
}));
|
|
2126
2164
|
tracks = looked;
|
|
2165
|
+
// Named by hand, so a single channel says what it is rather than what
|
|
2166
|
+
// its URL ends in.
|
|
2167
|
+
if (called !== "" && tracks.length === 1 && tracks[0]) {
|
|
2168
|
+
tracks = [{ ...tracks[0], title: called, named: true }];
|
|
2169
|
+
}
|
|
2127
2170
|
let added = tracks.length;
|
|
2128
2171
|
if (replacing) {
|
|
2129
2172
|
engine.replace(tracks, source);
|
|
2130
2173
|
}
|
|
2131
2174
|
else {
|
|
2132
|
-
added = engine.add(tracks, source);
|
|
2175
|
+
added = engine.add(tracks, source, called);
|
|
2133
2176
|
if (added === 0) {
|
|
2134
2177
|
// Everything there was already here. Not an error -- the playlist
|
|
2135
2178
|
// is exactly what the caller asked for -- but worth saying, so a
|
package/package.json
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface RemoteTrack {
|
|
|
39
39
|
* somebody's disk is their business.
|
|
40
40
|
*/
|
|
41
41
|
folder?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Whether this comes off the network rather than off a disk.
|
|
44
|
+
*
|
|
45
|
+
* A channel and a film in a folder are different kinds of thing and belong
|
|
46
|
+
* in different lists -- one is live and one is on demand. Which it is has to
|
|
47
|
+
* be a property of the track, because it was being inferred from how it
|
|
48
|
+
* happened to be loaded: a re-stream that replaced the playlist carried no
|
|
49
|
+
* group, so it appeared in no list of live streams at all and there was
|
|
50
|
+
* nowhere to click it.
|
|
51
|
+
*/
|
|
52
|
+
remote?: boolean;
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/** Everything a remote needs to draw the player. */
|
package/src/server.ts
CHANGED
|
@@ -426,6 +426,15 @@ export type Loaded = Track & {
|
|
|
426
426
|
* the source is added, rather than guessed from a URL that has no opinion.
|
|
427
427
|
*/
|
|
428
428
|
picture?: boolean;
|
|
429
|
+
/**
|
|
430
|
+
* Whether a person named this, rather than a tagger.
|
|
431
|
+
*
|
|
432
|
+
* Tags are read in the background and merged in when they arrive, which is
|
|
433
|
+
* right for a library and wrong for a channel: somebody called it "MLB
|
|
434
|
+
* Network", ffprobe came back a moment later with "932", and the name they
|
|
435
|
+
* chose vanished on its own.
|
|
436
|
+
*/
|
|
437
|
+
named?: boolean;
|
|
429
438
|
};
|
|
430
439
|
|
|
431
440
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
@@ -461,8 +470,11 @@ export interface Engine {
|
|
|
461
470
|
* What somebody means by putting a folder in a box: the album shows up at
|
|
462
471
|
* the bottom of the playlist under its own name, and the music that was
|
|
463
472
|
* already there is still there. Answers how many tracks were new.
|
|
473
|
+
*
|
|
474
|
+
* `called` is what a person named it, which beats whatever the address ends
|
|
475
|
+
* in -- a channel is "MLB Network", not "932".
|
|
464
476
|
*/
|
|
465
|
-
add(tracks: Track[], from: string): number;
|
|
477
|
+
add(tracks: Track[], from: string, called?: string): number;
|
|
466
478
|
/**
|
|
467
479
|
* Take an added source back out again, by the name `add` gave it.
|
|
468
480
|
*
|
|
@@ -500,6 +512,9 @@ export function toRemoteTracks(tracks: Loaded[]): RemoteTrack[] {
|
|
|
500
512
|
// how a client knows they are the library.
|
|
501
513
|
...(t.group ? { group: t.group } : {}),
|
|
502
514
|
...(t.folder ? { folder: t.folder } : {}),
|
|
515
|
+
// Said plainly rather than guessed at from how it was loaded: this is
|
|
516
|
+
// what puts a channel in the live list and a film in the library.
|
|
517
|
+
...(isRemote(t.path) ? { remote: true } : {}),
|
|
503
518
|
}));
|
|
504
519
|
}
|
|
505
520
|
|
|
@@ -767,8 +782,12 @@ export class PlayerEngine implements Engine {
|
|
|
767
782
|
* Paths already loaded are skipped, so adding the same album twice is not
|
|
768
783
|
* two copies of it.
|
|
769
784
|
*/
|
|
770
|
-
add(tracks: Track[], from: string): number {
|
|
771
|
-
|
|
785
|
+
add(tracks: Track[], from: string, called = ""): number {
|
|
786
|
+
// What somebody called it beats what the address happens to end in. A
|
|
787
|
+
// channel at .../932 is "932" to a URL and "MLB Network" to a person, and
|
|
788
|
+
// no transport stream reliably says which -- the one in front of us calls
|
|
789
|
+
// itself "Service01".
|
|
790
|
+
const group = called || sourceLabel(from);
|
|
772
791
|
const known = new Set(this.tracks.map((track) => track.path));
|
|
773
792
|
const fresh = tracks
|
|
774
793
|
.filter((track) => !known.has(track.path))
|
|
@@ -868,6 +887,9 @@ export class PlayerEngine implements Engine {
|
|
|
868
887
|
...tagged,
|
|
869
888
|
...(track.group ? { group: track.group } : {}),
|
|
870
889
|
...(track.folder ? { folder: track.folder } : {}),
|
|
890
|
+
// A name somebody chose outlives whatever the stream says about
|
|
891
|
+
// itself. It called itself "932".
|
|
892
|
+
...(track.named ? { title: track.title, named: true } : {}),
|
|
871
893
|
};
|
|
872
894
|
});
|
|
873
895
|
if (!changed) return;
|
|
@@ -879,6 +901,26 @@ export class PlayerEngine implements Engine {
|
|
|
879
901
|
}
|
|
880
902
|
}
|
|
881
903
|
|
|
904
|
+
/**
|
|
905
|
+
* The streams a server is carrying, gathered by name.
|
|
906
|
+
*
|
|
907
|
+
* A whole re-streamed folder is one entry rather than twenty: somebody
|
|
908
|
+
* looking at what is on wants "that album from the web", not every track in
|
|
909
|
+
* it. An entry names where to start, so clicking it plays.
|
|
910
|
+
*/
|
|
911
|
+
export function liveOnes(engine: Engine): { name: string; at: number; tracks: number }[] {
|
|
912
|
+
const tracks = engine.snapshot().tracks ?? [];
|
|
913
|
+
const found = new Map<string, { name: string; at: number; tracks: number }>();
|
|
914
|
+
tracks.forEach((track, at) => {
|
|
915
|
+
if (track.remote !== true) return;
|
|
916
|
+
const name = track.group || track.title;
|
|
917
|
+
const already = found.get(name);
|
|
918
|
+
if (already) already.tracks += 1;
|
|
919
|
+
else found.set(name, { name, at, tracks: 1 });
|
|
920
|
+
});
|
|
921
|
+
return [...found.values()];
|
|
922
|
+
}
|
|
923
|
+
|
|
882
924
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
883
925
|
export class EmptyEngine implements Engine {
|
|
884
926
|
constructor(private readonly note = "No library on this server — open files, or point this remote at your own nixamp.") {}
|
|
@@ -2137,11 +2179,14 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2137
2179
|
// what made moving between a channel and an album so confusing. Named
|
|
2138
2180
|
// here with the first track it owns, so it can be played from the list
|
|
2139
2181
|
// of what is live rather than hunted for among five thousand files.
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2182
|
+
// Every stream coming off the network, however it got here.
|
|
2183
|
+
//
|
|
2184
|
+
// This used to read the groups, which only exist for sources that were
|
|
2185
|
+
// added -- so re-streaming something as a replacement produced a
|
|
2186
|
+
// channel that appeared in no list anywhere, with nothing to click.
|
|
2187
|
+
// What makes a thing live is where it comes from, not which button
|
|
2188
|
+
// loaded it.
|
|
2189
|
+
restreams: liveOnes(engine),
|
|
2145
2190
|
});
|
|
2146
2191
|
return;
|
|
2147
2192
|
}
|
|
@@ -2496,10 +2541,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2496
2541
|
return;
|
|
2497
2542
|
}
|
|
2498
2543
|
let source = "";
|
|
2544
|
+
let called = "";
|
|
2499
2545
|
let replacing = false;
|
|
2500
2546
|
try {
|
|
2501
|
-
const body = JSON.parse(await readBody(request)) as {
|
|
2547
|
+
const body = JSON.parse(await readBody(request)) as {
|
|
2548
|
+
source?: unknown; replace?: unknown; name?: unknown;
|
|
2549
|
+
};
|
|
2502
2550
|
source = String(body.source ?? "");
|
|
2551
|
+
// Trimmed and capped: it is a label, and it can end up in a listing.
|
|
2552
|
+
called = String(body.name ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 80);
|
|
2503
2553
|
// Adding is what somebody means by putting a folder in a box, so it is
|
|
2504
2554
|
// the default. Replacing is the much larger claim that this server now
|
|
2505
2555
|
// serves that instead, so it is the one you have to ask for.
|
|
@@ -2535,11 +2585,17 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2535
2585
|
);
|
|
2536
2586
|
tracks = looked;
|
|
2537
2587
|
|
|
2588
|
+
// Named by hand, so a single channel says what it is rather than what
|
|
2589
|
+
// its URL ends in.
|
|
2590
|
+
if (called !== "" && tracks.length === 1 && tracks[0]) {
|
|
2591
|
+
tracks = [{ ...tracks[0], title: called, named: true }];
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2538
2594
|
let added = tracks.length;
|
|
2539
2595
|
if (replacing) {
|
|
2540
2596
|
engine.replace(tracks, source);
|
|
2541
2597
|
} else {
|
|
2542
|
-
added = engine.add(tracks, source);
|
|
2598
|
+
added = engine.add(tracks, source, called);
|
|
2543
2599
|
if (added === 0) {
|
|
2544
2600
|
// Everything there was already here. Not an error -- the playlist
|
|
2545
2601
|
// is exactly what the caller asked for -- but worth saying, so a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-DFWY01QV.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-ZK5W33OV.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-DWE7mc-Q.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}:{},...t.remote===!0?{remote:!0}:{}}}):void 0;return{revision:r(t.revision,0),...a?{tracks:a}:{},trackCount:r(t.trackCount,a?.length??0),index:r(t.index,0),playing:t.playing===!0,position:r(t.position,0),bars:Array.isArray(t.bars)?t.bars.map(e=>r(e,0)):[],levels:[r(i[0],0),r(i[1],0)],silent:t.silent===!0,note:typeof t.note==`string`?t.note:``,root:typeof t.root==`string`?t.root:n.root}}var 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 N=`nixamp.remote`,he=`nixamp.volume`,ge=`nixamp.listenHere`,_e=!1;function P(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function F(){let n={status:P(`status`),source:P(`source`),install:P(`install`),video:P(`video`),audio:P(`audio`),title:P(`title-line`),album:P(`album-line`),elapsed:P(`elapsed`),total:P(`total`),seek:P(`seek`),fullscreen:P(`fullscreen`),canvas:P(`spectrum`),glyphs:P(`glyphs`),levels:P(`levels`),playlist:P(`playlist`),crumbs:P(`crumbs`),filter:P(`filter`),playlistTitle:P(`playlist-panel`),note:P(`note`),files:P(`files`),folder:P(`folder`),remoteUrl:P(`remote-url`),remoteForm:P(`remote-form`),remoteState:P(`remote-state`),disconnect:P(`disconnect`),browse:P(`browse`),accountForm:P(`account-form`),accountEmail:P(`account-email`),accountPassword:P(`account-password`),accountSubmit:P(`account-submit`),accountToggle:P(`account-toggle`),accountProviders:P(`account-providers`),accountPanel:P(`account-panel`),accountElsewhere:P(`account-elsewhere`),accountSignOut:P(`account-signout`),accountNote:P(`account-note`),adminPanel:P(`admin-panel`),adminNote:P(`admin-note`),adminSaid:P(`admin-said`),adminConnections:P(`admin-connections`),publishPanel:P(`publish-panel`),publishNote:P(`publish-note`),publishList:P(`publish-list`),adminRestream:P(`admin-restream`),adminReplace:P(`admin-replace`),adminSource:P(`admin-source`),adminName:P(`admin-name`),homeNote:P(`home-note`),loadHome:P(`load-home`),directory:P(`directory`),recentNote:P(`recent-note`),recentList:P(`recent-list`),followingNote:P(`following-note`),followingList:P(`following-list`),serversPanel:P(`servers-panel`),serversNote:P(`servers-note`),serversList:P(`servers-list`),notifyPanel:P(`notify-panel`),notifyNote:P(`notify-note`),notifyWeb:P(`notify-web`),notifyEmail:P(`notify-email`),notifySms:P(`notify-sms`),notifyPhone:P(`notify-phone`),notifyPhoneForm:P(`notify-phone-form`),notifyPhoneNote:P(`notify-phone-note`),directoryNote:P(`directory-note`),directoryList:P(`directory-list`),onairPanel:P(`onair-panel`),onairNote:P(`onair-note`),onairList:P(`onair-list`),sharePanel:P(`share-panel`),shareNote:P(`share-note`),shareLink:P(`share-link`),shareCopy:P(`share-copy`),sharePhone:P(`share-phone`),shareSend:P(`share-send`),liveControls:P(`live-controls`),goLive:P(`go-live`),stopLive:P(`stop-live`),shareTo:P(`share-to`),listenOnly:P(`listen-only`),listenHere:P(`listen-here`),volume:P(`volume`),prev:P(`prev`),playPause:P(`play-pause`),stop:P(`stop`),next:P(`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),I()},onEnded:()=>A(1),onState:()=>I(),onError:e=>{l=e,I(),qe()}}),v=new oe({onSnapshot:e=>{o=ie(o,e),g()&&(p=e.bars.length>0?e.bars:p,m=M(m,p)),I()},onStatus:(e,t)=>{s=e,c=t??``,I()}}),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),B(t.video),we(),I())}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),B(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()),I())}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],I()}let F=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function I(){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===``,z(),n.glyphs.textContent=p.map(F).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 L=``,ve=-1,R=``;function ye(e){if(n.crumbs.hidden=!e,!e)return;let t=R===``?[]:R.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`,()=>{R=t,L=``,z()}),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`,()=>{R=R===``?e:`${R}/${e}`,L=``,z()}),n}function z(){let a=r===`remote`?o.tracks.map(e=>({name:t(e),seconds:e.duration,group:e.group??``,folder:e.folder??``,remote:e.remote===!0})):i.map(e=>({name:t(e),seconds:e.duration,group:``,folder:``,remote:!1})),s=n.filter.value.trim().toLowerCase(),c=a.map((e,t)=>({...e,index:t})).filter(e=>!e.remote).filter(e=>s===``||`${e.folder}/${e.name}`.toLowerCase().includes(s)),l=e=>s!==``||R===``||e===R||e.startsWith(`${R}/`),u=e=>s!==``||e===R,d=e=>{let t=R===``?e:e.slice(R.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}:${R}:${s}:${[...f].join(`,`)}:${p.map(e=>`${e.index}@${e.name}@${e.seconds}@${e.group}`).join(`|`)}`;if(m!==L){L=m,ye(s===``&&([...f.keys()].length>0||R!==``));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();U(t.ok?`Removed ${n.removed??0} tracks from ${e}.`:n.error??`that did not work`)}catch{U(`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(F).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 B(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`,()=>{L=``,z()}),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(he,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.`,I();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.`,I();return}(async()=>{s=`connecting`,I();let e=ue(i);if(e){s=`error`,c=e,l=e,r=`local`,I();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`,I();return}let n=await le(i,a);if(n){s=`error`,c=n,l=n,r=`local`,I();return}r=`remote`,l=``;try{localStorage.setItem(N,t.trim())}catch{}v.connect(t),Q(),Z(!0),W(),Y(),I()})()});let V=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&&q&&t.ownerId!==q&&e.append(Le(t.ownerId,t.name)),t.ownerId&&q&&t.ownerId===q){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 V()}})()}),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),V()}let H=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 U(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)),Qe(t.home??``,t.root??``),Q()}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 W=async()=>{if(r!==`remote`){n.adminPanel.hidden=!0,n.publishPanel.hidden=!0,H&&clearInterval(H),H=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,H&&clearInterval(H),H=null,Y(),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(),H=setInterval(()=>void je(),2e3)};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();if(!t)return;U(`Reading ${t}…`);let r=n.adminReplace.checked,i=n.adminName.value.trim();(async()=>{try{let e=await fetch(v.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...i?{name:i}:{},...r?{replace:!0}:{}})}),a=await e.json();U(e.ok?r?`Now serving ${t}.`:a.added===0?`Everything there was already in the playlist.`:`Added ${a.added??0} tracks from ${t}.`:a.error??`that did not work`),e.ok&&(n.adminSource.value=``,n.adminName.value=``,Y(),Q())}catch{U(`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=q?e.filter(e=>e.ownerId&&e.ownerId!==q):[];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{}},G=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 G({wantsWeb:e});return}await Ve(),await G({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{G({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 G({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),G({phone:n.notifyPhone.value.trim()})});let K=!1,q=``,J=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}.`:K?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=K?`Create account`:`Sign in`,n.accountToggle.textContent=K?`I have one`:`Create one`,n.accountPassword.autocomplete=K?`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();q=e.ok?t.account?.id??``:``,J(e.ok?t.account?.email??`you`:null)}catch{q=``,J(null)}Ge()};function Ge(){if(f===``)return;let e=f;f=``,n.remoteUrl.value=e,n.remoteForm.requestSubmit()}n.accountToggle.addEventListener(`click`,()=>{K=!K,J(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/${K?`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}q=i.account?.id??``,n.accountPassword.value=``,J(i.account?.email??t),W(),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{}q=``,J(null),v.close(),d=-1,r=`local`,s=`idle`,c=``,n.remoteUrl.value=``,n.sharePanel.hidden=!0,n.publishPanel.hidden=!0,n.adminPanel.hidden=!0,n.onairPanel.hidden=!0,n.listenOnly.hidden=!0,Z(!1);try{localStorage.removeItem(N)}catch{}l=`Signed out, and disconnected from the server.`,W(),I()})()});try{let e=new URL(globalThis.location.href).searchParams.get(`url`)??``;e!==``&&(f=e,n.remoteUrl.value=e,l=`Opening the stream you were sent…`,globalThis.history?.replaceState(null,``,globalThis.location.pathname))}catch{}Ue(),We(),W(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}V(),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,Z(!1),r=`local`,s=`idle`,c=``,I()});async function Y(){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 `),et(i),document.createTextNode(` and key `),et(a.code),document.createTextNode(`. That is a room with everyone else watching — not the stream itself.`)),n.shareSend.hidden=!1}}let Ke=``,X=null,Z=e=>{X&&clearInterval(X),X=null,e&&(X=setInterval(()=>void Q(),6e3))};async function Q(){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=[],s=e.server.playing,c=!n.adminPanel.hidden;o.push(Xe({title:e.server.name,detail:[s?`playing ${e.server.nowPlaying}`:e.server.nowPlaying?`stopped on ${e.server.nowPlaying}`:`nothing loaded`,`${e.server.tracks} track${e.server.tracks===1?``:`s`}`,e.server.code?`☎ ${e.server.code}`:`not listed`].join(` · `),playLabel:s?`Join live`:c?`Start the stream`:`Nothing playing`,onPlay:()=>{if(s){Ye(e.server.nowPlaying);return}c&&Je()},link:e.server.live?e.server.url:``}));for(let e of i)o.push(Xe({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(Xe({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),B(!0)},link:``}));n.onairList.replaceChildren(...o)}async function qe(){if(r===`remote`)try{if((await fetch(v.media(b(),0),{method:`GET`,headers:{range:`bytes=0-1`}})).status!==402)return;l=q===``?`This stream is busy enough to be charging for. Sign in to nixamp.com to pay for a pass.`:`This stream is charging for a pass. Follow the payment prompt to keep listening.`,q===``&&n.accountPanel.scrollIntoView({behavior:`smooth`,block:`center`}),I()}catch{}}async function Je(){U(`Starting the stream on the server…`);try{await v.send({type:`play`,index:Math.max(0,b())})}catch{U(`could not reach the server`);return}await Ye(o.tracks[b()]?.title??``),U(`Playing to the room. Anybody with the view link sees this.`),await Q()}async function Ye(e){d=-1,await _.load({title:e||`Live`,artist:``,album:``,duration:0,url:v.url(`/api/live`),video:!0,objectUrl:!1},!0),B(!0),l=`Watching what this server is playing. Everyone here sees the same thing.`,I()}function Xe(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=e.playLabel??`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 Ze=``;function Qe(e,t){Ze=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`,()=>{Ze!==``&&(n.loadHome.disabled=!0,U(`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:Ze})}),t=await e.json();U(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{U(`could not reach the server`)}finally{n.loadHome.disabled=!1}})())});let $e=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 Y()}};n.goLive.addEventListener(`click`,()=>void $e(!0)),n.stopLive.addEventListener(`click`,()=>void $e(!1));function et(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(ge,n.listenHere.checked?`1`:`0`)}catch{}r===`remote`&&(async()=>{n.listenHere.checked?(await v.send({type:`stop`}),await D(o.index)):(_.stop(),d=-1),I()})()}),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(he);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),_.volume=Number(e));let t=localStorage.getItem(N);t&&(n.remoteUrl.value=t),localStorage.getItem(ge)===`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),I())})(),(()=>{if(_e)return;let e=async()=>{try{let e=await(await fetch(`/jingles/index.json`)).json();if(Array.isArray(e)&&e.length>0){let t=e[Math.floor(Math.random()*e.length)];if(typeof t==`string`)return`/jingles/${t}`}}catch{}return``},t=new Audio;t.volume=.7;let n=()=>{_e=!0},r=()=>{document.removeEventListener(`pointerdown`,r),document.removeEventListener(`keydown`,r),n(),t.play().catch(()=>{})};e().then(e=>{if(e!==``)return t.src=e,t.play().then(n,()=>{document.addEventListener(`pointerdown`,r,{once:!0}),document.addEventListener(`keydown`,r,{once:!0})})})})(),I(),requestAnimationFrame(Ce)}F(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
|