nixamp 0.11.1 → 0.12.0

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/enrich.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { isMatchupName } from "./matchup.ts";
2
+ export { isMatchupName };
1
3
  /** Where the answers come from, unless a deployment says otherwise. */
2
4
  export declare const DEFAULT_SITE = "https://nichedb.dev";
3
5
  /** How long a hit is believed. Titles and channels change on the order of months. */
@@ -16,6 +18,12 @@ export declare const MIN_SCORE = 0.5;
16
18
  export declare const PREFIX_SCORE = 0.42;
17
19
  /** How many answers the cache keeps before the oldest go. */
18
20
  export declare const MAX_ENTRIES = 5000;
21
+ /**
22
+ * Which rules wrote the cache. Answers chosen by older rules are dropped on
23
+ * load: 0.11.0 remembered "Oppenheimer" as the row without the poster for
24
+ * seven days, and an update that chose better could not be seen through it.
25
+ */
26
+ export declare const CACHE_VERSION = 2;
19
27
  export type EnrichKind = "auto" | "title" | "channel" | "fixture";
20
28
  export interface Enriched {
21
29
  /** What nichedb says it is. */
@@ -105,4 +113,3 @@ export declare class Enricher {
105
113
  /** Write the cache now. Called on a timer, and by whoever is shutting down. */
106
114
  save(): void;
107
115
  }
108
- export {};
package/dist/enrich.js CHANGED
@@ -16,6 +16,8 @@
16
16
  */
17
17
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
18
18
  import { dirname } from "node:path";
19
+ import { isMatchupName } from "./matchup.js";
20
+ export { isMatchupName };
19
21
  /** Where the answers come from, unless a deployment says otherwise. */
20
22
  export const DEFAULT_SITE = "https://nichedb.dev";
21
23
  /** How long a hit is believed. Titles and channels change on the order of months. */
@@ -34,6 +36,12 @@ export const MIN_SCORE = 0.5;
34
36
  export const PREFIX_SCORE = 0.42;
35
37
  /** How many answers the cache keeps before the oldest go. */
36
38
  export const MAX_ENTRIES = 5000;
39
+ /**
40
+ * Which rules wrote the cache. Answers chosen by older rules are dropped on
41
+ * load: 0.11.0 remembered "Oppenheimer" as the row without the poster for
42
+ * seven days, and an update that chose better could not be seen through it.
43
+ */
44
+ export const CACHE_VERSION = 2;
37
45
  /** The collection and kind a name is asked about, from what the caller knows. */
38
46
  export function whereToAsk(kind, parsedKind) {
39
47
  const k = kind === "auto" ? parsedKind ?? "" : kind;
@@ -68,6 +76,14 @@ export function pickBest(answer, asked) {
68
76
  const isExact = (item) => item !== undefined && String(item.title ?? "").toLowerCase() === wanted;
69
77
  const plain = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
70
78
  const askedPlain = plain(wanted);
79
+ // nichedb keeps every meeting of two teams under the same title: the one
80
+ // being played now outranks the one next month, which outranks last year's.
81
+ const stateRank = (item) => {
82
+ if (item?.kind !== "fixture")
83
+ return 1;
84
+ const state = String(item.data?.["state"] ?? item.tags?.find((t) => t.startsWith("state:"))?.slice(6) ?? "");
85
+ return state === "in" ? 0 : state === "pre" ? 1 : state === "post" ? 2 : 1;
86
+ };
71
87
  for (const item of items) {
72
88
  const exact = isExact(item);
73
89
  const score = Number(item.score ?? 0);
@@ -86,10 +102,14 @@ export function pickBest(answer, asked) {
86
102
  best = item;
87
103
  else if (exact && !isExact(best))
88
104
  best = item;
89
- else if (exact && isExact(best) && !best.image_url && item.image_url)
105
+ else if (exact && isExact(best) && stateRank(item) < stateRank(best))
106
+ best = item;
107
+ else if (exact && isExact(best) && stateRank(item) === stateRank(best) && !best.image_url && item.image_url)
90
108
  best = item;
91
109
  else if (!isExact(best) && score > Number(best.score ?? 0))
92
110
  best = item;
111
+ else if (!isExact(best) && score === Number(best.score ?? 0) && stateRank(item) < stateRank(best))
112
+ best = item;
93
113
  }
94
114
  if (!best)
95
115
  return null;
@@ -174,10 +194,35 @@ export class Enricher {
174
194
  first.set("collection", where.collection);
175
195
  first.set("kind", where.kind);
176
196
  }
197
+ // Two sides with "vs" or "@" between them are a fixture until nichedb's
198
+ // sports collection says it has none: nichedb's parser reads a name, and
199
+ // "Chiefs vs Bills" reads as a title to a parser that expects films.
200
+ if (kind === "auto" && isMatchupName(name)) {
201
+ const fixture = new URLSearchParams(first);
202
+ fixture.set("collection", "sports");
203
+ fixture.set("kind", "fixture");
204
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
205
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE)
206
+ return hit;
207
+ }
177
208
  const answer = await this.get(`/api/v1/match?${first}`);
178
209
  if (where)
179
210
  return pickBest(answer, answer.parsed?.name ?? name);
180
- const guessed = whereToAsk("auto", answer.parsed?.kind);
211
+ // nichedb reads "Alien vs Predator" as a game too. Once the sports
212
+ // collection has said it has none, the name is a title after all.
213
+ let parsedKind = answer.parsed?.kind;
214
+ if (parsedKind === "fixture") {
215
+ if (!isMatchupName(name)) {
216
+ const fixture = new URLSearchParams(first);
217
+ fixture.set("collection", "sports");
218
+ fixture.set("kind", "fixture");
219
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
220
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE)
221
+ return hit;
222
+ }
223
+ parsedKind = "title";
224
+ }
225
+ const guessed = whereToAsk("auto", parsedKind);
181
226
  if (!guessed)
182
227
  return null;
183
228
  const second = new URLSearchParams(first);
@@ -217,7 +262,10 @@ export class Enricher {
217
262
  return;
218
263
  try {
219
264
  const parsed = JSON.parse(readFileSync(this.options.cacheFile, "utf8"));
220
- for (const [k, v] of Object.entries(parsed)) {
265
+ // A file from older rules is a file of answers those rules chose.
266
+ if (parsed.v !== CACHE_VERSION)
267
+ return;
268
+ for (const [k, v] of Object.entries(parsed.entries ?? {})) {
221
269
  if (v && typeof v.at === "number")
222
270
  this.cache.set(k, v);
223
271
  }
@@ -242,7 +290,7 @@ export class Enricher {
242
290
  try {
243
291
  mkdirSync(dirname(this.options.cacheFile), { recursive: true });
244
292
  const tmp = `${this.options.cacheFile}.tmp`;
245
- writeFileSync(tmp, JSON.stringify(Object.fromEntries(this.cache)));
293
+ writeFileSync(tmp, JSON.stringify({ v: CACHE_VERSION, entries: Object.fromEntries(this.cache) }));
246
294
  renameSync(tmp, this.options.cacheFile);
247
295
  this.dirty = false;
248
296
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Does this name read as two sides playing each other?
3
+ *
4
+ * "NFL: Chiefs vs Bills", "Lakers @ Celtics", "Rangers at Celtic 19:45": a
5
+ * fixture, which nichedb's sports collection keeps with a score. "Live at
6
+ * Wembley" and "Dinner at Eight" are not, and a wrong score line under a
7
+ * concert is worse than none.
8
+ *
9
+ * Shared by the server, which decides where to ask, and the browser, which
10
+ * decides how to ask about a channel. No imports, so it bundles anywhere.
11
+ */
12
+ export declare function isMatchupName(name: string): boolean;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Does this name read as two sides playing each other?
3
+ *
4
+ * "NFL: Chiefs vs Bills", "Lakers @ Celtics", "Rangers at Celtic 19:45": a
5
+ * fixture, which nichedb's sports collection keeps with a score. "Live at
6
+ * Wembley" and "Dinner at Eight" are not, and a wrong score line under a
7
+ * concert is worse than none.
8
+ *
9
+ * Shared by the server, which decides where to ask, and the browser, which
10
+ * decides how to ask about a channel. No imports, so it bundles anywhere.
11
+ */
12
+ /** A league or country before a colon: "NFL: ", "UK: ", "EPL - ". */
13
+ const PREFIX = /^[A-Za-z0-9 .&'-]{1,20}(?::|\s-)\s+/;
14
+ /** A trailing time, with or without am/pm and a zone: "19:45", "7:30 PM EDT", "(20:00)". */
15
+ const TIME = /(?:\s+|\s*[-|(]\s*)\d{1,2}(?::\d{2})?\s*(?:[ap]\.?m\.?)?(?:\s+[A-Z]{2,4})?\)?\s*$/i;
16
+ /** What sits between the two sides. "at" is the weakest of these and reads twice below. */
17
+ const APART = /\s+(?:vs\.?|v\.?|at|@)\s+/i;
18
+ /** A side that begins like this is a sentence, not a team. */
19
+ const NOT_A_TEAM = /^(?:the|a|an|live|tonight|recorded|filmed|concert|home|dinner|breakfast|lunch|midnight|night|one night|death|murder|meet me|panic|sunset|sunrise)\b/i;
20
+ function twoSides(text) {
21
+ // Twice: "Chiefs vs Bills - 7:30 PM EDT" has the dash and the time and the zone.
22
+ const parts = text.replace(TIME, "").replace(TIME, "").trim().split(APART);
23
+ if (parts.length !== 2)
24
+ return false;
25
+ return parts.every((side) => {
26
+ const s = side.trim();
27
+ return s.length >= 2 && s.length <= 48 && /[A-Za-z]/.test(s) && !NOT_A_TEAM.test(s);
28
+ });
29
+ }
30
+ export function isMatchupName(name) {
31
+ const text = String(name ?? "").trim();
32
+ if (text === "")
33
+ return false;
34
+ // With the league in front and without: "NFL: Chiefs vs Bills" reads either
35
+ // way, and "Chiefs vs. Bills - 7:30 PM" must not lose its teams as a prefix.
36
+ return twoSides(text.replace(PREFIX, "")) || twoSides(text);
37
+ }
package/dist/server.js CHANGED
@@ -35,7 +35,7 @@ import { readSession } from "./session.js";
35
35
  import { Directory, ENDED_TTL_MS, parseAnnouncement } from "./directory.js";
36
36
  import { PartyLine, telnyxSms } from "./partyline.js";
37
37
  import { HlsPackagers, withKey } from "./hls.js";
38
- import { DEFAULT_SITE as NICHEDB, Enricher } from "./enrich.js";
38
+ import { DEFAULT_SITE as NICHEDB, Enricher, FIXTURE_TTL_MS } from "./enrich.js";
39
39
  import { contentTypeFor, downloadArgs, fileNameFor, inputArgsFor, linkChannelId, playableLink, resolveLink, saveFormat, } from "./links.js";
40
40
  import { CALL_IN_NUMBER, OPT_IN_PATH, optInPage } from "./optin.js";
41
41
  import pg from "pg";
@@ -2341,8 +2341,13 @@ export function createHandler(engine, options) {
2341
2341
  response.writeHead(200, {
2342
2342
  ...CORS,
2343
2343
  "content-type": "application/json; charset=utf-8",
2344
- // A miss is worth asking again in a few hours; a hit lasts the day.
2345
- "cache-control": match ? "public, max-age=3600" : "public, max-age=600",
2344
+ // Briefly: the server remembers for days, so the browser need not,
2345
+ // and an hour of browser cache hid a better answer for an hour. A
2346
+ // fixture's score moves by the minute and the page asks again every
2347
+ // minute; a browser cache that long would answer instead of the server.
2348
+ "cache-control": match?.kind === "fixture"
2349
+ ? `public, max-age=${Math.floor(FIXTURE_TTL_MS / 2000)}`
2350
+ : match ? "public, max-age=300" : "public, max-age=120",
2346
2351
  });
2347
2352
  response.end(JSON.stringify({ match }));
2348
2353
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/enrich.ts CHANGED
@@ -16,6 +16,9 @@
16
16
  */
17
17
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
18
18
  import { dirname } from "node:path";
19
+ import { isMatchupName } from "./matchup.ts";
20
+
21
+ export { isMatchupName };
19
22
 
20
23
  /** Where the answers come from, unless a deployment says otherwise. */
21
24
  export const DEFAULT_SITE = "https://nichedb.dev";
@@ -35,6 +38,12 @@ export const MIN_SCORE = 0.5;
35
38
  export const PREFIX_SCORE = 0.42;
36
39
  /** How many answers the cache keeps before the oldest go. */
37
40
  export const MAX_ENTRIES = 5000;
41
+ /**
42
+ * Which rules wrote the cache. Answers chosen by older rules are dropped on
43
+ * load: 0.11.0 remembered "Oppenheimer" as the row without the poster for
44
+ * seven days, and an update that chose better could not be seen through it.
45
+ */
46
+ export const CACHE_VERSION = 2;
38
47
 
39
48
  export type EnrichKind = "auto" | "title" | "channel" | "fixture";
40
49
 
@@ -115,6 +124,13 @@ export function pickBest(answer: MatchAnswer, asked: string): Enriched | null {
115
124
  item !== undefined && String(item.title ?? "").toLowerCase() === wanted;
116
125
  const plain = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
117
126
  const askedPlain = plain(wanted);
127
+ // nichedb keeps every meeting of two teams under the same title: the one
128
+ // being played now outranks the one next month, which outranks last year's.
129
+ const stateRank = (item: Item | undefined): number => {
130
+ if (item?.kind !== "fixture") return 1;
131
+ const state = String(item.data?.["state"] ?? item.tags?.find((t) => t.startsWith("state:"))?.slice(6) ?? "");
132
+ return state === "in" ? 0 : state === "pre" ? 1 : state === "post" ? 2 : 1;
133
+ };
118
134
  for (const item of items) {
119
135
  const exact = isExact(item);
120
136
  const score = Number(item.score ?? 0);
@@ -131,8 +147,10 @@ export function pickBest(answer: MatchAnswer, asked: string): Enriched | null {
131
147
  // a film and only one has the poster; among the rest, the score decides.
132
148
  if (!best) best = item;
133
149
  else if (exact && !isExact(best)) best = item;
134
- else if (exact && isExact(best) && !best.image_url && item.image_url) best = item;
150
+ else if (exact && isExact(best) && stateRank(item) < stateRank(best)) best = item;
151
+ else if (exact && isExact(best) && stateRank(item) === stateRank(best) && !best.image_url && item.image_url) best = item;
135
152
  else if (!isExact(best) && score > Number(best.score ?? 0)) best = item;
153
+ else if (!isExact(best) && score === Number(best.score ?? 0) && stateRank(item) < stateRank(best)) best = item;
136
154
  }
137
155
  if (!best) return null;
138
156
  const kind = best.kind === "channel" || best.kind === "fixture" ? best.kind : "title";
@@ -224,9 +242,32 @@ export class Enricher {
224
242
  first.set("collection", where.collection);
225
243
  first.set("kind", where.kind);
226
244
  }
245
+ // Two sides with "vs" or "@" between them are a fixture until nichedb's
246
+ // sports collection says it has none: nichedb's parser reads a name, and
247
+ // "Chiefs vs Bills" reads as a title to a parser that expects films.
248
+ if (kind === "auto" && isMatchupName(name)) {
249
+ const fixture = new URLSearchParams(first);
250
+ fixture.set("collection", "sports");
251
+ fixture.set("kind", "fixture");
252
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
253
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE) return hit;
254
+ }
227
255
  const answer = await this.get(`/api/v1/match?${first}`);
228
256
  if (where) return pickBest(answer, answer.parsed?.name ?? name);
229
- const guessed = whereToAsk("auto", answer.parsed?.kind);
257
+ // nichedb reads "Alien vs Predator" as a game too. Once the sports
258
+ // collection has said it has none, the name is a title after all.
259
+ let parsedKind = answer.parsed?.kind;
260
+ if (parsedKind === "fixture") {
261
+ if (!isMatchupName(name)) {
262
+ const fixture = new URLSearchParams(first);
263
+ fixture.set("collection", "sports");
264
+ fixture.set("kind", "fixture");
265
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
266
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE) return hit;
267
+ }
268
+ parsedKind = "title";
269
+ }
270
+ const guessed = whereToAsk("auto", parsedKind);
230
271
  if (!guessed) return null;
231
272
  const second = new URLSearchParams(first);
232
273
  second.set("collection", guessed.collection);
@@ -263,8 +304,13 @@ export class Enricher {
263
304
  private load(): void {
264
305
  if (!this.options.cacheFile) return;
265
306
  try {
266
- const parsed = JSON.parse(readFileSync(this.options.cacheFile, "utf8")) as Record<string, Cached>;
267
- for (const [k, v] of Object.entries(parsed)) {
307
+ const parsed = JSON.parse(readFileSync(this.options.cacheFile, "utf8")) as {
308
+ v?: number;
309
+ entries?: Record<string, Cached>;
310
+ };
311
+ // A file from older rules is a file of answers those rules chose.
312
+ if (parsed.v !== CACHE_VERSION) return;
313
+ for (const [k, v] of Object.entries(parsed.entries ?? {})) {
268
314
  if (v && typeof v.at === "number") this.cache.set(k, v);
269
315
  }
270
316
  } catch {
@@ -287,7 +333,7 @@ export class Enricher {
287
333
  try {
288
334
  mkdirSync(dirname(this.options.cacheFile), { recursive: true });
289
335
  const tmp = `${this.options.cacheFile}.tmp`;
290
- writeFileSync(tmp, JSON.stringify(Object.fromEntries(this.cache)));
336
+ writeFileSync(tmp, JSON.stringify({ v: CACHE_VERSION, entries: Object.fromEntries(this.cache) }));
291
337
  renameSync(tmp, this.options.cacheFile);
292
338
  this.dirty = false;
293
339
  } catch (error) {
package/src/matchup.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Does this name read as two sides playing each other?
3
+ *
4
+ * "NFL: Chiefs vs Bills", "Lakers @ Celtics", "Rangers at Celtic 19:45": a
5
+ * fixture, which nichedb's sports collection keeps with a score. "Live at
6
+ * Wembley" and "Dinner at Eight" are not, and a wrong score line under a
7
+ * concert is worse than none.
8
+ *
9
+ * Shared by the server, which decides where to ask, and the browser, which
10
+ * decides how to ask about a channel. No imports, so it bundles anywhere.
11
+ */
12
+
13
+ /** A league or country before a colon: "NFL: ", "UK: ", "EPL - ". */
14
+ const PREFIX = /^[A-Za-z0-9 .&'-]{1,20}(?::|\s-)\s+/;
15
+ /** A trailing time, with or without am/pm and a zone: "19:45", "7:30 PM EDT", "(20:00)". */
16
+ const TIME = /(?:\s+|\s*[-|(]\s*)\d{1,2}(?::\d{2})?\s*(?:[ap]\.?m\.?)?(?:\s+[A-Z]{2,4})?\)?\s*$/i;
17
+ /** What sits between the two sides. "at" is the weakest of these and reads twice below. */
18
+ const APART = /\s+(?:vs\.?|v\.?|at|@)\s+/i;
19
+ /** A side that begins like this is a sentence, not a team. */
20
+ const NOT_A_TEAM = /^(?:the|a|an|live|tonight|recorded|filmed|concert|home|dinner|breakfast|lunch|midnight|night|one night|death|murder|meet me|panic|sunset|sunrise)\b/i;
21
+
22
+ function twoSides(text: string): boolean {
23
+ // Twice: "Chiefs vs Bills - 7:30 PM EDT" has the dash and the time and the zone.
24
+ const parts = text.replace(TIME, "").replace(TIME, "").trim().split(APART);
25
+ if (parts.length !== 2) return false;
26
+ return parts.every((side) => {
27
+ const s = side.trim();
28
+ return s.length >= 2 && s.length <= 48 && /[A-Za-z]/.test(s) && !NOT_A_TEAM.test(s);
29
+ });
30
+ }
31
+
32
+ export function isMatchupName(name: string): boolean {
33
+ const text = String(name ?? "").trim();
34
+ if (text === "") return false;
35
+ // With the league in front and without: "NFL: Chiefs vs Bills" reads either
36
+ // way, and "Chiefs vs. Bills - 7:30 PM" must not lose its teams as a prefix.
37
+ return twoSides(text.replace(PREFIX, "")) || twoSides(text);
38
+ }
package/src/server.ts CHANGED
@@ -52,7 +52,7 @@ import { readSession } from "./session.ts";
52
52
  import { Directory, ENDED_TTL_MS, parseAnnouncement, type Listing } from "./directory.ts";
53
53
  import { PartyLine, telnyxSms } from "./partyline.ts";
54
54
  import { HlsPackagers, withKey } from "./hls.ts";
55
- import { DEFAULT_SITE as NICHEDB, Enricher, type EnrichKind } from "./enrich.ts";
55
+ import { DEFAULT_SITE as NICHEDB, Enricher, type EnrichKind, FIXTURE_TTL_MS } from "./enrich.ts";
56
56
  import {
57
57
  contentTypeFor, downloadArgs, fileNameFor, inputArgsFor, linkChannelId, playableLink, resolveLink, saveFormat,
58
58
  type ResolvedLink,
@@ -2815,8 +2815,13 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2815
2815
  response.writeHead(200, {
2816
2816
  ...CORS,
2817
2817
  "content-type": "application/json; charset=utf-8",
2818
- // A miss is worth asking again in a few hours; a hit lasts the day.
2819
- "cache-control": match ? "public, max-age=3600" : "public, max-age=600",
2818
+ // Briefly: the server remembers for days, so the browser need not,
2819
+ // and an hour of browser cache hid a better answer for an hour. A
2820
+ // fixture's score moves by the minute and the page asks again every
2821
+ // minute; a browser cache that long would answer instead of the server.
2822
+ "cache-control": match?.kind === "fixture"
2823
+ ? `public, max-age=${Math.floor(FIXTURE_TTL_MS / 2000)}`
2824
+ : match ? "public, max-age=300" : "public, max-age=120",
2820
2825
  });
2821
2826
  response.end(JSON.stringify({ match }));
2822
2827
  return;
@@ -1 +1 @@
1
- import{t as e}from"./index-2_Frv88t.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
1
+ import{t as e}from"./index-CuS16Oq3.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-6UPkDW0o.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-CJBpg2W3.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 ne=2048;function re(e,t){return e||t===`hls`||t===`mpegts`}var ie=class{elements;handlers;attached=null;source=``;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(ae(t))});let e=e=>()=>{t===this.active&&this.handlers.onBusy?.(e)};for(let n of[`loadstart`,`waiting`,`stalled`,`seeking`])t.addEventListener(n,e(!0));for(let n of[`playing`,`canplay`,`pause`,`ended`,`error`,`emptied`,`seeked`,`abort`])t.addEventListener(n,e(!1))}}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=ne,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){this.source=e.objectUrl?``:e.url;let n=e.objectUrl?e.video?`mp4`:`audio`:d({src:e.url}),r=re(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.source=``,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 ae(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 oe(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function se(e,t){return{...t,tracks:t.tracks??e.tracks}}function ce(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 x(e,t,n=``){let r=`${e===``?``:ce(e)}${t.startsWith(`/`)?t:`/${t}`}`;return n?`${r}${r.includes(`?`)?`&`:`?`}k=${encodeURIComponent(n)}`:r}function le(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:ce(`${n.origin}${n.pathname}`),key:decodeURIComponent(i)}}function S(e,t,n=0,r=``){return x(e,n>0?`/api/media/${t}?kbps=${Math.round(n)}`:`/api/media/${t}`,r)}function C(e){if(typeof e!=`object`||!e)return null;let t=e,n=oe(),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 ue=class{handlers;source=null;base=``;key=``;shape=`/admin/`;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}url(e){return x(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}=le(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(x(t,`/api/events`,n));this.source=r,r.onopen=()=>this.handlers.onStatus(`live`),r.onmessage=e=>{let t=C(w(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(x(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=C(await t.json());n&&this.handlers.onSnapshot(n)}media(e,t=0){return S(this.base,e,t,this.key)}close(){this.source?.close(),this.source=null}};function w(e){try{return JSON.parse(e)}catch{return null}}async function de(e,t,n=``){try{let r=await fetch(x(e,`/api/state`,n),{signal:t});return r.ok?C(await r.json()):null}catch{return null}}function fe(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 pe(e,t=``,n){let r;try{r=await fetch(x(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 me(e,t,n=``){try{let r=await fetch(x(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 he(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 T=.14,E=.02;function ge(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 _e(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 ve(e,t,n=T){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function ye(e,t,n=E){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function be(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)})}function D(e,t){return{name:String(e?.name??e?.displayName??e?.abbreviation??``),score:typeof e?.score==`number`?e.score:typeof t==`number`?t:null,logo:typeof e?.logoUrl==`string`&&/^https?:\/\//.test(e.logoUrl)?e.logoUrl:``}}function xe(e){let t=String(e.data.state??e.tags?.find(e=>e.startsWith(`state:`))?.slice(6)??``);return t===`in`||t===`post`?t:`pre`}function Se(e,t={}){if(!e)return``;let n=new Date(e);if(Number.isNaN(n.getTime()))return``;let r=t.now??new Date,i=t.timeZone?{timeZone:t.timeZone}:{},a=n.toLocaleDateString(t.locale,i)===r.toLocaleDateString(t.locale,i);return n.toLocaleString(t.locale,{...i,...a?{}:{weekday:`short`},hour:`numeric`,minute:`2-digit`})}function Ce(e,t={}){let n=e.data,r=D(n.away,n.awayScore),i=D(n.home,n.homeScore),a=xe(e),o=typeof n.statusDetail==`string`?n.statusDetail.trim():``,s;if(a===`in`)s=o?`LIVE · ${o}`:`LIVE`;else if(a===`post`)s=`FINAL`;else{let n=Se(e.published_at,t);s=n?`Kicks off ${n}`:o||`Upcoming`}let c=[],l=n.league,u=String(l?.abbreviation??l?.name??``);u&&c.push(u),typeof n.broadcast==`string`&&n.broadcast.trim()&&c.push(n.broadcast.trim());let d=a!==`pre`,f=e=>d&&e.score!==null?` ${e.score}`:``;return{away:r,home:i,state:a,status:s,chips:c,text:`${r.name}${f(r)} – ${i.name}${f(i)}`}}var O=/^[A-Za-z0-9 .&'-]{1,20}(?::|\s-)\s+/,k=/(?:\s+|\s*[-|(]\s*)\d{1,2}(?::\d{2})?\s*(?:[ap]\.?m\.?)?(?:\s+[A-Z]{2,4})?\)?\s*$/i,we=/\s+(?:vs\.?|v\.?|at|@)\s+/i,Te=/^(?:the|a|an|live|tonight|recorded|filmed|concert|home|dinner|breakfast|lunch|midnight|night|one night|death|murder|meet me|panic|sunset|sunrise)\b/i;function A(e){let t=e.replace(k,``).replace(k,``).trim().split(we);return t.length===2&&t.every(e=>{let t=e.trim();return t.length>=2&&t.length<=48&&/[A-Za-z]/.test(t)&&!Te.test(t)})}function Ee(e){let t=String(e??``).trim();return t===``?!1:A(t.replace(O,``))||A(t)}var De=`nixamp.remote`,Oe=`nixamp.volume`,ke=`nixamp.listenHere`,Ae=!1;function j(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function M(){let n={status:j(`status`),source:j(`source`),install:j(`install`),video:j(`video`),audio:j(`audio`),title:j(`title-line`),album:j(`album-line`),meta:j(`meta-line`),metaBlurb:j(`meta-blurb`),liveLine:j(`live-line`),downloadNow:j(`download-now`),linkForm:j(`link-form`),linkUrl:j(`link-url`),goLiveNow:j(`go-live-now`),elapsed:j(`elapsed`),total:j(`total`),seek:j(`seek`),fullscreen:j(`fullscreen`),copyNow:j(`copy-now`),canvas:j(`spectrum`),glyphs:j(`glyphs`),levels:j(`levels`),playlist:j(`playlist`),crumbs:j(`crumbs`),filter:j(`filter`),playlistTitle:j(`playlist-panel`),note:j(`note`),files:j(`files`),folder:j(`folder`),remoteUrl:j(`remote-url`),remoteForm:j(`remote-form`),remoteState:j(`remote-state`),disconnect:j(`disconnect`),browse:j(`browse`),accountForm:j(`account-form`),accountEmail:j(`account-email`),accountPassword:j(`account-password`),accountSubmit:j(`account-submit`),accountToggle:j(`account-toggle`),accountProviders:j(`account-providers`),accountPanel:j(`account-panel`),accountElsewhere:j(`account-elsewhere`),accountSignOut:j(`account-signout`),accountNote:j(`account-note`),adminPanel:j(`admin-panel`),adminNote:j(`admin-note`),adminSaid:j(`admin-said`),adminConnections:j(`admin-connections`),publishPanel:j(`publish-panel`),publishNote:j(`publish-note`),publishList:j(`publish-list`),adminRestream:j(`admin-restream`),adminReplace:j(`admin-replace`),adminSource:j(`admin-source`),adminName:j(`admin-name`),adminAdd:j(`admin-add`),homeNote:j(`home-note`),loadHome:j(`load-home`),directory:j(`directory`),recentNote:j(`recent-note`),recentList:j(`recent-list`),followingNote:j(`following-note`),followingList:j(`following-list`),serversPanel:j(`servers-panel`),serversNote:j(`servers-note`),serversList:j(`servers-list`),favoritesPanel:j(`favorites-panel`),favoritesNote:j(`favorites-note`),favoritesList:j(`favorites-list`),favHere:j(`fav-here`),catalogsPanel:j(`catalogs-panel`),catalogsNote:j(`catalogs-note`),catalogsForm:j(`catalogs-form`),catalogSource:j(`catalog-source`),catalogName:j(`catalog-name`),catalogsList:j(`catalogs-list`),catalogsCrumbs:j(`catalogs-crumbs`),catalogsFilter:j(`catalogs-filter`),catalogsEntries:j(`catalogs-entries`),notifyPanel:j(`notify-panel`),notifyNote:j(`notify-note`),notifyWeb:j(`notify-web`),notifyEmail:j(`notify-email`),notifySms:j(`notify-sms`),notifyPhone:j(`notify-phone`),notifyPhoneForm:j(`notify-phone-form`),notifyPhoneNote:j(`notify-phone-note`),directoryNote:j(`directory-note`),directoryList:j(`directory-list`),onairPanel:j(`onair-panel`),onairNote:j(`onair-note`),onairList:j(`onair-list`),sharePanel:j(`share-panel`),shareNote:j(`share-note`),shareLink:j(`share-link`),shareCopy:j(`share-copy`),sharePhone:j(`share-phone`),shareSend:j(`share-send`),liveControls:j(`live-controls`),goLive:j(`go-live`),stopLive:j(`stop-live`),shareTo:j(`share-to`),listenOnly:j(`listen-only`),listenHere:j(`listen-here`),volume:j(`volume`),prev:j(`prev`),playPause:j(`play-pause`),stop:j(`stop`),next:j(`next`)},r={live:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="2.5"/><path d="M8.5 15.5a5 5 0 0 1 0-7"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M5.6 18.4a9 9 0 0 1 0-12.8"/><path d="M18.4 5.6a9 9 0 0 1 0 12.8"/></svg>`,link:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>`,copy:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>`,restart:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v6h-6"/></svg>`,remove:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,check:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5L20 7"/></svg>`},i=(e,t)=>{e.innerHTML=r[t]},a=document.title||`nixamp`,o=`local`,s=``,c=!1,l=``,u=0,d=``,f=[],p=0,m=oe(),h=`idle`,g=``,_=`Pick files, or connect to a nixamp running somewhere else.`,v=!1,y=-1,b=null,ne=0,re=0,ae=!1,ce=()=>re>0||ae;async function x(e){re+=1,B();try{return await e()}finally{--re,B()}}let S=null,C=null,w=null,T=``,E=null;function D(e,t,n=null,r=!1){let i=`${t}|${e}`;if(T=i,E&&clearTimeout(E),E=null,!r){if(w?.key===i)return;w=null}if(o!==`remote`||e.trim()===``)return;let a=new URLSearchParams({name:e,kind:t});n&&a.set(`year`,String(n)),fetch(F.url(`/api/enrich?${a}`)).then(e=>e.ok?e.json():{match:null}).then(r=>{if(T!==i)return;w={key:i,match:r.match??null},B();let a=w.match;a?.kind===`fixture`&&xe(a)!==`post`&&(E=setTimeout(()=>{E=null,!(T!==i||P.source===``&&!b)&&D(e,t,n,!0)},6e4))}).catch(()=>void 0)}let Se=!1,O=``,k=``,we=null,Te=``,A=Array(24).fill(0),M=Array(24).fill(0),je=[],N=()=>o===`remote`&&!n.listenHere.checked,Me=()=>o===`remote`&&!n.adminPanel.hidden;function Ne(){try{if(localStorage.getItem(`nixamp.hls`)===`1`)return!0}catch{}return typeof MediaSource<`u`?!1:n.video.canPlayType(`application/vnd.apple.mpegurl`)!==``}function Pe(){if(o!==`remote`)return null;if(S?.catalog&&S.entry)return{kind:`entry`,catalog:S.catalog,entry:S.entry};if(b)return{kind:`channel`,id:b.id,name:b.name};let e=m.tracks[L()];return e&&(P.source!==``||N())?{kind:`track`,index:L(),name:t(e)}:null}async function Fe(e,t){t.disabled=!0;let n=e.kind===`entry`?e.entry.title:e.name;_=`Putting ${n} on the air…`,B();try{let r=``;if(e.kind===`track`)await F.send({type:`play`,index:e.index}),r=`live`;else{let t=e.kind===`entry`?`/api/catalogs/${encodeURIComponent(e.catalog.id)}/entries/${encodeURIComponent(e.entry.id)}/live`:`/api/channels/${encodeURIComponent(e.id)}/keep`,i=await fetch(F.url(t),{method:`POST`}),a=await i.json().catch(()=>({}));if(!i.ok){_=a.error??`${n} would not go on the air.`,B();return}r=`channel:${e.kind===`entry`?a.channel??``:e.id}`}Se?await fetch(F.url(`/api/live/start`),{method:`POST`}).catch(()=>void 0):await yn(!0),await $t(),$();let i=pn(r),a=O?` Call ${k||`the line`} and key ${O} to talk about it.`:``;i===``?_=`${n} is on the air.${a}`:(await fn(i,t,`✓`),_=`${n} is on the air. Link copied.${a}`),B()}catch{_=`could not reach the server`,B()}finally{t.disabled=!1}}function Ie(e,t){let n=document.createElement(`button`);return n.type=`button`,n.className=`row-copy row-live`,i(n,`live`),n.title=`Go live with ${t}: on the air for everyone, listed, link copied`,n.setAttribute(`aria-label`,`Go live with ${t}`),n.addEventListener(`click`,t=>{t.stopPropagation(),Fe(e(),n)}),n}let P=new ie({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=f[p];o===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),B()},onEnded:()=>{cn()||o===`remote`&&y<0&&!N()||z(1)},onState:()=>B(),onBusy:e=>{ae!==e&&(ae=e,B())},onError:e=>{cn()||(_=e,B(),rn())}}),F=new ue({onSnapshot:e=>{if(m=se(m,e),l.startsWith(`track:`)&&m.tracks.length>0){let e=Number(l.slice(6));if(l=``,Number.isInteger(e)&&e>=0&&e<m.tracks.length){let t=u;Ue(e).then(()=>{t>0&&(P.seek(t),setTimeout(()=>P.seek(t),600))})}}N()&&(A=e.bars.length>0?e.bars:A,M=ye(M,A)),B()},onStatus:(e,t)=>{h=e,g=t??``,B()}}),I=()=>o===`remote`?m.tracks.length:f.length,L=()=>o===`remote`?N()||y<0?m.index:Math.min(y,Math.max(0,m.tracks.length-1)):p,Le=()=>{if(b)return b.name;let e=o===`remote`?m.tracks[L()]:f[L()];return e?t(e):`Nothing loaded.`},Re=()=>b?`live on this server`:S?.kind===`live`&&o===`remote`?`live on ${s||`this server`}`:(o===`remote`?m.tracks[L()]:f[L()])?.album||`—`;function ze(){if(o!==`remote`||b===null&&S?.kind!==`live`){n.liveLine.hidden=!0,n.liveLine.replaceChildren();return}let e=s||`this server`,t=b?b.name:C?.server.nowPlaying||Le(),r=[b?`Live on ${e}: `:`Live from ${e}, now playing: `,bn(t)],i=b?C?.channels.find(e=>e.id===b?.id)?.code??``:Se?O:``;i&&r.push(`. To talk about it, call `,bn(k||`the line`),` and key `,bn(i),`.`);let a=r.map(e=>typeof e==`string`?e:e.textContent).join(``);n.liveLine.dataset.drawn!==a&&(n.liveLine.dataset.drawn=a,n.liveLine.hidden=!1,n.liveLine.replaceChildren(...r.map(e=>typeof e==`string`?document.createTextNode(e):e)))}let Be=()=>N()?m.tracks[L()]?.duration??0:P.duration,Ve=()=>N()?m.position:P.position,He=()=>N()?m.playing:P.playing;async function R(e){if(o===`remote`){if(N()){await F.send({type:`play`,index:e});return}await Ue(e);return}let t=f[e];t&&(p=e,b=null,S={kind:`file`},await x(()=>P.load(t,!0)),it(t.video),at(),B())}async function Ue(e){let t=m.tracks[e];t&&(y=e,b=null,S={kind:`file`},D(t.title,`auto`),await x(()=>P.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:F.media(e,0),video:t.video===!0,objectUrl:!1},!0)),it(t.video===!0),at())}async function We(){if(N()){await F.send({type:`toggle`});return}I()!==0&&(P.playing?P.pause():P.position>0?await P.play():await R(L()),B())}async function z(e){let t=I();if(t!==0){if(N()){await F.send({type:e>0?`next`:`prev`});return}await R((L()+e+t)%t)}}async function Ge(){if(N()){await F.send({type:`stop`});return}b=null,S=null,P.stop(),A=Array(24).fill(0),M=[...A],B()}let Ke=(e,t)=>`L${`▮`.repeat(Math.round(e*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)}`,qe=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))],Je=``;function Ye(){let t=[],r=``,i=null,a=b?C?.channels.find(e=>e.id===b?.id):void 0,c=P.source===``&&!b&&!(N()&&m.tracks[L()]);if(!c){b?t.push(S?.entry?.live===!1?`ON DEMAND · LIVE CHANNEL`:`LIVE`):S?.kind===`vod`?t.push(`ON DEMAND`):S?.kind===`live`?t.push(`LIVE`):o===`remote`&&N()?t.push(`ON THE SERVER`):t.push(`FILE`),!n.video.hidden&&n.video.videoWidth>0?t.push(`${n.video.videoWidth}×${n.video.videoHeight}`):n.video.hidden?t.push(`audio`):t.push(`video`),a?(t.push(a.via===`pull`?`${a.listeners} watching`:`${a.listeners} listening · over ${a.via}`),a.startedAt>0&&t.push(`on air ${e(Math.max(0,(Date.now()-a.startedAt)/1e3))}`),a.redials&&t.push(`redialled ${a.redials}×`),Me()&&a.error&&t.push(a.error)):o===`remote`&&!b?(m.tracks[L()]&&I()>0&&t.push(`track ${L()+1} of ${I()}`),N()&&C&&t.push(`${C.server.playing?`playing`:`stopped`} on ${s||`the server`}`)):o===`local`&&I()>0&&t.push(`track ${L()+1} of ${I()}`),S?.catalog!==void 0&&(S.kind===`channel`?b!==null:S.kind===`vod`&&P.source!==``)&&S?.catalog&&(t.push(S.entry?.group?`${S.catalog.name} › ${S.entry.group}`:S.catalog.name),r=S.entry?.logo??``);let c=w?.key===T?w.match:null;if(c?.kind===`fixture`)i=Ce(c),t.unshift(i.status,...i.chips);else if(c){c.image&&(r=c.image);let e=c.data;if(c.kind===`title`){c.year&&t.push(String(c.year));let n=typeof e.rating==`number`?e.rating:null;n&&t.push(`★ ${n.toFixed(1)}`);let r=Array.isArray(e.genres)?e.genres.slice(0,2).map(String):[];r.length&&t.push(r.join(` · `));let i=typeof e.runtimeMin==`number`?e.runtimeMin:0;i&&t.push(`${i} min`)}else if(c.kind===`channel`){let n=typeof e.country==`string`?e.country:``,r=Array.isArray(e.categories)?e.categories.slice(0,2).map(String):[],i=typeof e.network==`string`?e.network:``;n&&t.push(n),r.length&&t.push(r.join(` · `)),i&&t.push(i)}}if(b&&S?.link){let e=``;try{e=new URL(S.link.url).hostname.replace(/^www\./,``)}catch{}t.push(S.link.extractor&&S.link.extractor!==`direct`&&S.link.extractor!==`generic`?`${S.link.extractor} · ${e}`:e||`link`)}Se&&O&&N()&&!b&&S?.kind!==`live`&&t.push(k?`☎ ${k} · key ${O}`:`☎ code ${O}`)}let l=w?.key===T?w.match:null,u=!c&&!i&&l?.summary?l.summary:``,d=`${r}|${i?`${i.away.logo}|${i.home.logo}|${i.text}`:``}|${t.join(`|`)}|${u}`;if(d===Je)return;Je=d,n.meta.hidden=t.length===0,n.metaBlurb.textContent=u,n.metaBlurb.hidden=u===``;let f=[];if(i){let e=document.createElement(`div`);e.className=`meta-score`;let t=(e,t)=>{let n=[],r=document.createElement(`span`);if(r.className=`meta-team`,r.textContent=e.name,n.push(r),i?.state!==`pre`&&e.score!==null){let t=document.createElement(`b`);t.className=`meta-points`,t.textContent=String(e.score),n.push(t)}if(e.logo!==``){let r=document.createElement(`img`);r.className=`meta-team-logo`,r.alt=``,r.src=e.logo,r.addEventListener(`error`,()=>{r.hidden=!0}),t?n.unshift(r):n.push(r)}return n},n=document.createElement(`span`);n.className=`meta-dash`,n.textContent=`–`,e.replaceChildren(...t(i.away,!0),n,...t(i.home,!1)),f.push(e)}if(r!==``&&/^https?:\/\//.test(r)){let e=document.createElement(`img`);e.className=l?.kind===`title`&&r===l.image?`meta-logo meta-poster`:`meta-logo`,e.alt=``,e.src=r,e.addEventListener(`error`,()=>{e.hidden=!0}),f.push(e)}for(let e of t){let t=document.createElement(`span`);t.className=i?.state===`in`&&e===i.status?`meta-chip chip-live`:`meta-chip`,t.textContent=e,f.push(t)}n.meta.replaceChildren(...f)}function B(){let t=I(),r=He(),i=ce();n.status.textContent=i?`LOADING`:r?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=i?`loading`:String(r),n.title.textContent=Le(),ze(),Ye(),n.goLiveNow.hidden=!Me()||Pe()===null;let c=r?`${Le()} · ${a}`:a;document.title!==c&&(document.title=c),n.copyNow.hidden=P.source===``,n.downloadNow.hidden=!(b&&S?.link?.download),n.album.textContent=Re();let l=Ve(),u=Be();n.elapsed.textContent=e(l),n.total.textContent=u>0?e(u):`--:--`,v||(n.seek.value=String(u>0?Math.round(l/u*1e3):0),n.seek.disabled=u<=0||N()),n.playPause.textContent=r?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,r?`Pause`:`Play`),n.playlistTitle.dataset.title=o===`remote`?`Files on ${s||`this server`} (${t.toLocaleString()})`:`Playlist (${t})`,n.source.textContent=o===`remote`?`connected · ${s||F.address.replace(/^https?:\/\//,``)||`—`}`:f.length>0?`local · ${f.length} files`:`no source`,n.remoteState.textContent=o===`remote`?`${h}${g?` — ${g}`:``}`:`not connected`,n.remoteState.dataset.status=o===`remote`?h:`idle`,n.disconnect.hidden=o!==`remote`;let d=o===`remote`&&m.note!==``?m.note:_;n.note.textContent=d,n.note.hidden=d===``,et(),n.glyphs.textContent=A.map(qe).join(``);let[p,ee]=N()?m.levels:P.levels();n.levels.textContent=Ke(p,ee)}let Xe=``,Ze=-1,V=``;function Qe(e){if(n.crumbs.hidden=!e,!e)return;let t=V===``?[]:V.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`,()=>{V=t,Xe=``,et()}),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 $e(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`,()=>{V=V===``?e:`${V}/${e}`,Xe=``,et()}),n}function et(){let r=o===`remote`?m.tracks.map(e=>({name:t(e),seconds:e.duration,group:e.group??``,folder:e.folder??``,remote:e.remote===!0})):f.map(e=>({name:t(e),seconds:e.duration,group:``,folder:``,remote:!1})),a=n.filter.value.trim().toLowerCase(),s=r.map((e,t)=>({...e,index:t})).filter(e=>!e.remote).filter(e=>a===``||`${e.folder}/${e.name}`.toLowerCase().includes(a)),c=e=>a!==``||V===``||e===V||e.startsWith(`${V}/`),l=e=>a!==``||e===V,u=e=>{let t=V===``?e:e.slice(V.length+1),n=t.indexOf(`/`);return n===-1?t:t.slice(0,n)},d=new Map;for(let e of s){if(!c(e.folder)||l(e.folder))continue;let t=u(e.folder);t!==``&&d.set(t,(d.get(t)??0)+1)}let p=s.filter(e=>l(e.folder)&&c(e.folder)),h=`${o}:${V}:${a}:${[...d].join(`,`)}:${p.map(e=>`${e.index}@${e.name}@${e.seconds}@${e.group}`).join(`|`)}`;if(h!==Xe){Xe=h,Qe(a===``&&([...d.keys()].length>0||V!==``));let t=[];for(let[e,n]of[...d].sort((e,t)=>e[0].localeCompare(t[0],void 0,{numeric:!0})))t.push($e(e,n));let r=``,s=p.some(e=>e.group!==``);for(let n of p){n.group!==r&&(s||n.group!==``)&&(r=n.group,t.push(tt(n.group)));let a=document.createElement(`li`);a.className=`row`,a.dataset.index=String(n.index);let c=document.createElement(`span`);c.className=`n`,c.textContent=String(n.index+1).padStart(2,` `);let l=document.createElement(`span`);l.className=`name`,l.textContent=n.name;let u=document.createElement(`span`);if(u.className=`time`,u.textContent=n.seconds>0?e(n.seconds):`--:--`,a.append(c,l,u),o===`remote`){let e=document.createElement(`button`);e.type=`button`,e.className=`row-copy`,i(e,`copy`),e.title=`Copy a link that plays this here, from where it is`,e.setAttribute(`aria-label`,`Copy a link that plays ${n.name}`),e.addEventListener(`click`,t=>{t.stopPropagation(),fn(pn(`track:${n.index}`,y===n.index?P.position:0),e,`✓`)}),a.append(e),Me()&&a.append(Ie(()=>({kind:`track`,index:n.index,name:n.name}),n.name))}t.push(a)}n.playlist.replaceChildren(...t)}let g=L(),_=He(),v;for(let e of Array.from(n.playlist.children)){let t=e,n=Number(t.dataset.index),r=Number.isInteger(n)&&n===g;t.classList.toggle(`selected`,r),t.classList.toggle(`playing`,r&&_),r&&(v=t)}g!==Ze&&(Ze=g,v?.scrollIntoView({block:`nearest`}))}function tt(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(),nt(e)}),t.append(n)}return t}async function nt(e){try{let t=await fetch(F.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 rt(){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 o=t.getContext(`2d`);if(N())M=ye(M,A);else{let e=P.read();e.length>0&&(je.length!==25&&(je=ge(24,e.length)),A=ve(A,_e(e,je)),M=ye(M,A))}if(o){let e=getComputedStyle(document.documentElement);be(o,{width:t.width,height:t.height},A,M,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(He()){n.glyphs.textContent=A.map(qe).join(``);let[t,r]=N()?m.levels:P.levels();n.levels.textContent=Ke(t,r),n.elapsed.textContent=e(Ve());let i=Be();!v&&i>0&&(n.seek.value=String(Math.round(Ve()/i*1e3)))}requestAnimationFrame(rt)}function it(e){n.video.hidden=!e,n.fullscreen.hidden=!e}function at(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:Le(),album:Re(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void We()),navigator.mediaSession.setActionHandler(`pause`,()=>void We()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void z(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void z(-1)))}n.filter.addEventListener(`input`,()=>{Xe=``,et()}),n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&R(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 z(-1)),n.next.addEventListener(`click`,()=>void z(1)),n.stop.addEventListener(`click`,()=>void Ge()),n.playPause.addEventListener(`click`,()=>void We());async function ot(e){if(o!==`remote`){_=`Connect to a server first: it fetches the link and plays it to you.`,B();return}_=`Reading ${e}…`,await x(async()=>{let t,n={};try{t=await fetch(F.url(`/api/links/play`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({url:e})}),n=await t.json().catch(()=>({}))}catch{_=`could not reach the server`;return}if(!t.ok||!n.channel){_=n.error??`that link would not play.`;return}await sn({id:n.channel,name:n.name||e,video:n.video!==!1},!0,{kind:`channel`,link:{url:e,extractor:n.extractor??``,download:n.download===!0,live:n.live===!0,video:n.video!==!1}})}),B()}n.linkForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.linkUrl.value.trim();t!==``&&ot(t)}),n.downloadNow.addEventListener(`click`,()=>{let e=S?.link;if(!e||o!==`remote`)return;let t=e.video?``:`&audio=1`;globalThis.open(F.url(`/api/links/download?url=${encodeURIComponent(e.url)}${t}`),`_blank`),_=`Fetching it through the server; your browser will save it when it arrives.`,B()}),n.goLiveNow.addEventListener(`click`,()=>{let e=Pe();e&&Fe(e,n.goLiveNow)}),n.seek.addEventListener(`input`,()=>{v=!0}),n.seek.addEventListener(`change`,()=>{let e=Be();e>0&&P.seek(Number(n.seek.value)/1e3*e),v=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;P.volume=e;try{localStorage.setItem(Oe,String(e))}catch{}});let st=e=>{e.addEventListener(`change`,()=>{let t=ee(Array.from(e.files??[]));if(t.length===0){_=`Nothing playable in that selection.`,B();return}te(f),f=t,p=0,o=`local`,F.close(),_=``,R(0)})};st(n.files),st(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.remoteUrl.value,{base:r,key:i}=le(t);if(r===``){_=`That is not an address.`,B();return}(async()=>{h=`connecting`,B();let e=he(r);if(e){h=`error`,g=e,_=e,o=`local`,B();return}if(await me(r,void 0,i)===null){h=`error`;let e=fe(r);g=e?`needs the server's name`:`not answering`,_=e||`Nothing answered at ${r}. If that is your machine, it is off or nixamp is not running on it; otherwise check the address.`,o=`local`,B();return}let n=await pe(r,i);if(n){h=`error`,g=n,_=n,o=`local`,B();return}o=`remote`,_=``;try{localStorage.setItem(De,t.trim())}catch{}F.connect(t),$(),kt(),nn(!0),gt(),$t(),B()})()});let ct=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??[],yt(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} ${e.length===1?`server is`:`servers are`} on. Connect to one to browse its files and watch what is live on it. No account needed.`;for(let t of e){let e=document.createElement(`li`),r=document.createElement(`span`);r.className=`server-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=t.name;let a=document.createElement(`span`);a.className=`detail`;let o=[`${t.tracks.toLocaleString()} files to browse`];t.playing!==!1&&t.nowPlaying?o.push(`playing ${t.nowPlaying}`):o.push(`player idle`),t.code&&o.push(t.callers?`☎ ${t.code} · ${t.callers} on the phone`:`☎ ${t.code}`),a.textContent=o.join(` · `),r.append(i,a);let s=!!t.admin,u=(e,r=``)=>{c=e,l=r,n.remoteUrl.value=e?t.url:t.admin??t.url,n.directory.hidden=!0,n.remoteForm.requestSubmit()},d=document.createElement(`ul`);d.className=`server-lives`;for(let e of t.channels??[]){let n=document.createElement(`li`),r=document.createElement(`span`);r.className=`detail live`;let i=t.channelCodes?.[e]??``,a=t.channelCallers?.[e]??0;r.textContent=`● ${e}`+(i?` · ☎ ${i}${a?` · ${a} on the phone`:``}`:``);let o=document.createElement(`button`);o.type=`button`,o.className=`button`,o.textContent=`Play`,o.title=`Watch ${e}, live on ${t.name}`,o.addEventListener(`click`,()=>u(!0,`channel:${e}`)),n.append(r,o),d.append(n)}let f=document.createElement(`button`);f.type=`button`,f.className=`button`,f.textContent=`Viewer`,f.title=`Browse and watch. Changes nothing on the server.`,f.addEventListener(`click`,()=>u(!0));let p=document.createElement(`button`);if(p.type=`button`,p.className=`button`,p.textContent=`Admin`,p.disabled=!s,p.title=s?`Drive this server: what plays, what is live, what is on it.`:Q?`You do not administer this server.`:`Sign in as this server's owner to administer it.`,p.addEventListener(`click`,()=>u(!1)),e.append(r,f,p),Q&&e.append(St(t.url,t.name)),t.ownerId&&Q&&t.ownerId!==Q&&e.append(Ht(t.ownerId,t.name)),d.childElementCount>0&&e.append(d),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 ct()}})()}),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),ct()}let H=null,lt=e=>e===`cgnat`?`mobile or tailscale`:e===`private`?`your network`:e===`local`?`this machine`:e===`public`?`the internet`:e,ut=e=>e===`events`?`watching the panel`:e===`page`?`opened the page`:e===`media`?`playing a track`:e===`stream`?`listening live`:e,dt=``,ft=``,pt=e=>{let t=e.map(e=>`${e.address}|${e.kind}|${e.track}|${Math.round(e.bytes/4096)}|${e.endedAt}`).join(`~`);if(t===dt)return;dt=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,``],[lt(t.network),`network-${t.network}`],[ut(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 mt=async()=>{try{let e=await fetch(F.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.`,pt(t.connections??[]),ht(t.publish??[],(t.channels??[]).map(e=>e.id)),vn(t.home??``,t.root??``),$()}catch{n.adminNote.textContent=`lost touch with the server`}};function ht(e,t){n.publishPanel.hidden=e.length===0;let r=`${e.map(e=>`${e.id}=${e.url}`).join(`~`)}::${t.join(`,`)}`;if(r===ft)return;if(ft=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 gt=async()=>{if(o!==`remote`){n.adminPanel.hidden=!0,n.publishPanel.hidden=!0,H&&clearInterval(H),H=null;return}let e=!1,t=null,r=!1;try{let n=await fetch(F.url(`/api/admin`));if(n.ok){let i=await n.json();e=i.allowed===!0,t=i.as??null,r=i.claimed===!0}}catch{e=!1}if(c&&(e=!1),n.adminPanel.hidden=!e,H&&clearInterval(H),H=null,$(),kt(),$t(),n.listenOnly.hidden=e,!e){n.listenOnly.textContent=r?`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.`,mt(),H=setInterval(()=>void mt(),2e3)};function _t(e,t,r){let i=(t||e).toLowerCase().replace(/[^a-z0-9_-]+/g,`-`).replace(/^-+|-+$/g,``).slice(0,40)||`s${Math.random().toString(16).slice(2,8)}`;U(`Starting ${t||e}…`),(async()=>{try{let a=await fetch(F.url(`/api/channels/${encodeURIComponent(i)}/pull`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({...r===void 0?{source:e}:{at:r},...t?{name:t}:{}})}),o=await a.json();if(!a.ok){U(o.error??`that did not work`);return}U(`${o.channel?.name||t||e} is on the air.`),n.adminSource.value=``,n.adminName.value=``,$t(),$()}catch{U(`could not reach the server`)}})()}n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();t&&_t(t,n.adminName.value.trim())}),n.adminAdd.addEventListener(`click`,()=>{let e=n.adminSource.value.trim();if(!e)return;U(`Reading ${e}…`);let t=n.adminReplace.checked,r=n.adminName.value.trim();(async()=>{try{let i=await fetch(F.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:e,...r?{name:r}:{},...t?{replace:!0}:{}})}),a=await i.json();U(i.ok?t?`Now serving ${e}.`:a.added===0?`Everything there was already in the playlist.`:`Added ${a.added??0} tracks from ${e}.`:a.error??`that did not work`),i.ok&&(n.adminSource.value=``,n.adminName.value=``,$t(),$())}catch{U(`could not reach the server`)}})()});let vt=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`},yt=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 ${vt(e.endedAt)}`:`ended ${vt(e.endedAt)}`,r.append(i,a),t.append(r,Ht(e.ownerId,e.name)),n.recentList.append(t)}},W=new Set,G=e=>{try{return new URL(e).origin}catch{return e}},K=e=>[...W].some(t=>G(t)===G(e));async function bt(){if(!Q){W=new Set,n.favoritesPanel.hidden=!0,Ct();return}try{let e=await fetch(`/api/v1/favorites`);if(!e.ok){n.favoritesPanel.hidden=!0;return}let t=(await e.json()).favorites??[];W=new Set(t.map(e=>e.url)),n.favoritesPanel.hidden=t.length===0,n.favoritesNote.textContent=`Servers you hearted. Connect to one, or let it go.`,n.favoritesList.replaceChildren(...t.map(e=>{let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`server-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name||e.url.replace(/^https?:\/\//,``);let a=document.createElement(`span`);a.className=e.live?`detail live`:`detail`,a.textContent=e.live?[`● on now`,e.nowPlaying?`playing ${e.nowPlaying}`:``,e.channels.length>0?`live: ${e.channels.join(`, `)}`:``].filter(Boolean).join(` · `):`not on right now`,r.append(i,a);let o=document.createElement(`button`);return o.type=`button`,o.className=`button`,o.textContent=`Connect`,o.addEventListener(`click`,()=>{n.remoteUrl.value=e.url,n.remoteForm.requestSubmit()}),t.append(r,o,St(e.url,e.name)),t}))}catch{n.favoritesPanel.hidden=!0}Ct()}async function xt(e,t,n){try{if(!(n?await fetch(`/api/v1/favorites`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({url:e,name:t})}):await fetch(`/api/v1/favorites?url=${encodeURIComponent(e)}`,{method:`DELETE`})).ok){_=n?`Could not save that favourite.`:`Could not remove that favourite.`,B();return}}catch{_=`could not reach nixamp.com`,B();return}if(n)W.add(e);else for(let t of[...W])G(t)===G(e)&&W.delete(t);await bt()}function St(e,t){let n=document.createElement(`button`);n.type=`button`,n.className=`heart`;let r=()=>{let t=K(e);n.textContent=t?`♥`:`♡`,n.dataset.on=t?`yes`:`no`,n.title=t?`Remove from favourites`:`Add to favourites`,n.setAttribute(`aria-label`,n.title)};return r(),n.addEventListener(`click`,n=>{n.stopPropagation();let i=[...W].find(t=>G(t)===G(e))??e;xt(K(e)?i:e,t,!K(e)).then(r)}),n}function Ct(){let e=o===`remote`?F.shareLink:``;if(n.favHere.hidden=!(Q&&e),n.favHere.hidden)return;let t=K(e);n.favHere.textContent=t?`♥`:`♡`,n.favHere.dataset.on=t?`yes`:`no`,n.favHere.title=t?`Remove this server from your favourites`:`Add this server to your favourites`,n.favHere.setAttribute(`aria-label`,n.favHere.title)}i(n.copyNow,`copy`),n.copyNow.addEventListener(`click`,()=>{fn(P.source,n.copyNow,`✓`)}),n.favHere.addEventListener(`click`,()=>{let e=mn()||F.shareLink;if(!e)return;let t=[...W].find(t=>G(t)===G(e))??e;xt(K(e)?t:e,s||F.address,!K(e)).then(Ct)});let q=[],J=null,Y=null,wt=``,X=[],Tt=0,Et=null,Dt=0;function Ot(e){if(!e)return`never`;let t=Math.max(0,Math.round((Date.now()-e)/1e3));if(t<90)return`just now`;let n=Math.round(t/60);if(n<90)return`${n} min ago`;let r=Math.round(n/60);return r<36?`${r} h ago`:`${Math.round(r/24)} d ago`}async function kt(){if(o!==`remote`){n.catalogsPanel.hidden=!0;return}let e;try{e=await fetch(F.url(`/api/catalogs`))}catch{n.catalogsPanel.hidden=!0;return}if(!e.ok){n.catalogsPanel.hidden=!0;return}q=(await e.json().catch(()=>({}))).catalogs??[],J&&=q.find(e=>e.id===J?.id)??null,J||(Y=null),n.catalogsPanel.hidden=!1,At()}function At(){let e=!n.adminPanel.hidden;n.catalogsForm.hidden=!e;let t=q.reduce((e,t)=>e+t.live,0),r=q.reduce((e,t)=>e+t.vod,0);n.catalogsNote.textContent=q.length===0?e?`No catalogs yet. Add an m3u list of channels or films.`:`No catalogs yet.`:`${q.length} ${q.length===1?`catalog`:`catalogs`} · ${t} live ${t===1?`channel`:`channels`} · ${r} on demand`,jt();let i=J!==null&&Y!==null;if(n.catalogsList.hidden=i,n.catalogsFilter.hidden=!i,n.catalogsEntries.hidden=!i,i){It();return}if(J){Nt(J);return}n.catalogsList.replaceChildren(...q.map(t=>Mt(t,e)))}function jt(){let e=J!==null;if(n.catalogsCrumbs.hidden=!e,!e)return;let t=(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`,t),r},r=()=>{let e=document.createElement(`span`);return e.textContent=`/`,e},i=[t(`All catalogs`,()=>{J=null,Y=null,At()},!1),r(),t(J?.name??``,()=>{Y=null,At()},Y===null)];Y!==null&&i.push(r(),t(Y===``?`All groups`:Y,()=>void 0,!0)),n.catalogsCrumbs.replaceChildren(...i)}function Mt(e,t){let n=document.createElement(`li`),r=document.createElement(`span`);r.className=`server-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name;let a=document.createElement(`span`);if(a.className=`detail`,a.textContent=[`${e.entries.toLocaleString()} ${e.entries===1?`entry`:`entries`}`,`${e.live.toLocaleString()} live`,`${e.vod.toLocaleString()} on demand`,`refreshed ${Ot(e.refreshedAt)}`].join(` · `),r.append(i,a),t&&e.error){let t=document.createElement(`span`);t.className=`detail`,t.textContent=e.error,r.append(t)}let o=document.createElement(`button`);if(o.type=`button`,o.className=`button`,o.textContent=`Browse`,o.addEventListener(`click`,()=>{J=e,Y=null,At()}),n.append(r,o),t){let t=document.createElement(`button`);t.type=`button`,t.className=`ghost`,t.textContent=`Refresh`,t.title=`Read the list again`,t.addEventListener(`click`,()=>{Rt(e)});let r=document.createElement(`button`);r.type=`button`,r.className=`ghost`,r.textContent=`Remove`,r.title=`Take this catalog off the server`,r.addEventListener(`click`,()=>{confirm(`Remove ${e.name} from this server?`)&&zt(e)}),n.append(t,r)}return n}async function Nt(e){n.catalogsList.replaceChildren();let t=[];try{let n=await x(()=>fetch(F.url(`/api/catalogs/${encodeURIComponent(e.id)}/groups`)));if(!n.ok)throw Error(String(n.status));t=(await n.json()).groups??[]}catch{_=`Could not read the groups in ${e.name}.`,B();return}if(J?.id!==e.id||Y!==null)return;let r=[Pt(`All groups`,``,e.entries,e.live,e.vod),...t.map(e=>Pt(e.name||`(no group)`,e.name,e.count,e.live,e.vod))];n.catalogsList.replaceChildren(...r)}function Pt(e,t,r,i,a){let o=document.createElement(`li`),s=document.createElement(`span`);s.className=`server-label`;let c=document.createElement(`span`);c.className=`name`,c.textContent=e;let l=document.createElement(`span`);l.className=`detail`,l.textContent=`${r.toLocaleString()} · ${i.toLocaleString()} live · ${a.toLocaleString()} on demand`,s.append(c,l);let u=document.createElement(`button`);return u.type=`button`,u.className=`button`,u.textContent=`Open`,u.addEventListener(`click`,()=>{Y=t,wt=``,n.catalogsFilter.value=``,X=[],Tt=0,At(),Ft(0)}),o.append(s,u),o}async function Ft(e){let t=J,n=Y;if(!t||n===null)return;let r=++Dt,i=new URLSearchParams({group:n,q:wt,offset:String(e),limit:`200`}),a;try{let e=await x(()=>fetch(F.url(`/api/catalogs/${encodeURIComponent(t.id)}/entries?${i}`)));if(!e.ok)throw Error(String(e.status));a=await e.json()}catch{_=`Could not read ${t.name}.`,B();return}r===Dt&&(Tt=a.total??0,X=e===0?a.entries??[]:[...X,...a.entries??[]],It())}function It(){let t=J;if(!t)return;let r=X.map(n=>{let r=document.createElement(`li`);r.className=`row`;let a=document.createElement(`span`);a.className=`name`,a.textContent=n.title;let o=document.createElement(`span`);if(o.className=n.live?`catalog-tag catalog-live`:`catalog-tag`,o.textContent=n.live?`LIVE`:n.duration>0?e(n.duration):`VOD`,r.append(a,o),!n.live){let e=document.createElement(`button`);e.type=`button`,e.className=`row-copy`,i(e,`copy`),e.title=`Copy this entry's URL`,e.setAttribute(`aria-label`,`Copy the URL of ${n.title}`),e.addEventListener(`click`,r=>{r.stopPropagation();let i=`/api/catalogs/${encodeURIComponent(t.id)}/entries/${encodeURIComponent(n.id)}/stream`;fn(F.url(i),e,`✓`)}),r.append(e)}return Me()&&r.append(Ie(()=>({kind:`entry`,catalog:{id:t.id,name:t.name},entry:n}),n.title)),r.addEventListener(`click`,()=>{Lt(t,n,r)}),r});if(X.length===0){let e=document.createElement(`li`);e.className=`group`;let t=document.createElement(`span`);t.className=`group-name`,t.textContent=wt?`Nothing called "${wt}" here.`:`Nothing in this group.`,e.append(t),r.push(e)}else if(X.length<Tt){let e=document.createElement(`li`);e.className=`group`;let t=document.createElement(`button`);t.type=`button`,t.className=`ghost`,t.textContent=`Show more (${X.length.toLocaleString()} of ${Tt.toLocaleString()})`,t.addEventListener(`click`,e=>{e.stopPropagation(),Ft(X.length)}),e.append(t),r.push(e)}n.catalogsEntries.replaceChildren(...r)}async function Lt(e,t,n){n?.classList.add(`loading`),_=`Starting ${t.title}…`;let r={catalog:{id:e.id,name:e.name},entry:t};try{await x(async()=>{let n,i={};try{n=await fetch(F.url(`/api/catalogs/${encodeURIComponent(e.id)}/entries/${encodeURIComponent(t.id)}/play`),{method:`POST`}),i=await n.json().catch(()=>({}))}catch{_=`could not reach the server`;return}if(!n.ok){_=i.error??`${t.title} would not play.`;return}let a=i.name||t.title;if(i.kind===`live`&&i.channel){await sn({id:i.channel,name:a,video:!0},!0,{kind:`channel`,...r});return}if(i.kind===`vod`&&i.url){b=null,y=-1,S={kind:`vod`,...r},D(a,`title`),await P.load({title:a,artist:``,album:``,duration:0,url:F.url(i.url),video:!0,objectUrl:!1},!0),it(!0),_=`Playing ${a}.`;return}_=`${t.title} would not play.`})}finally{n?.classList.remove(`loading`),B()}}async function Rt(e){U(`Reading ${e.name} again…`);try{let t=await fetch(F.url(`/api/catalogs/${encodeURIComponent(e.id)}/refresh`),{method:`POST`}),n=await t.json().catch(()=>({}));U(t.ok?`${n.catalog?.name??e.name}: ${(n.catalog?.entries??0).toLocaleString()} entries.`:n.error??`that did not work`)}catch{U(`could not reach the server`)}kt()}async function zt(e){try{U((await fetch(F.url(`/api/catalogs/${encodeURIComponent(e.id)}`),{method:`DELETE`})).ok?`${e.name} is off the server.`:`that did not work`)}catch{U(`could not reach the server`)}J?.id===e.id&&(J=null,Y=null),kt()}n.catalogsForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.catalogSource.value.trim(),r=n.catalogName.value.trim();t&&(async()=>{U(`Reading ${r||t}…`);try{let e=await fetch(F.url(`/api/catalogs`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,name:r})}),i=await e.json().catch(()=>({}));if(!e.ok){U(i.error??`that did not work`);return}U(`${i.catalog?.name??r??t}: ${(i.catalog?.entries??0).toLocaleString()} entries.`),n.catalogSource.value=``,n.catalogName.value=``}catch{U(`could not reach the server`)}kt()})()}),n.catalogsFilter.addEventListener(`input`,()=>{Et&&clearTimeout(Et),Et=setTimeout(()=>{Et=null,wt=n.catalogsFilter.value.trim(),Ft(0)},250)});let Bt=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()}),me(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 Bt()}catch{s.disabled=!1}})()}),t.append(r,o,s),n.serversList.append(t)}}catch{n.serversPanel.hidden=!0}},Vt=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}},Ht=(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),Vt())}catch{}finally{n.disabled=!1}})()}),n},Ut=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},Wt=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,Gt=async()=>{if(!Wt())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:Ut(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}},Kt=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{}},qt=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`}},Jt=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=Wt()&&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 Gt();n.notifyWeb.checked=e,await qt({wantsWeb:e});return}await Kt(),await qt({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{qt({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 qt({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),qt({phone:n.notifyPhone.value.trim()})});let Z=!1,Q=``,Yt=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(Jt(),Vt(),Bt()):(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}.`:Z?`Create an account on nixamp.com.`:`Listening needs no account. Sign in to keep favourites, follow people, and publish.`,n.accountSubmit.textContent=Z?`Create account`:`Sign in`,n.accountToggle.textContent=Z?`I have one`:`Create one`,n.accountPassword.autocomplete=Z?`new-password`:`current-password`},Xt=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)}},Zt=async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();Q=e.ok?t.account?.id??``:``,Yt(e.ok?t.account?.email??`you`:null)}catch{Q=``,Yt(null)}bt(),Qt()};function Qt(){if(Te===``)return;let e=Te;Te=``,n.remoteUrl.value=e,n.remoteForm.requestSubmit()}n.accountToggle.addEventListener(`click`,()=>{Z=!Z,Yt(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/${Z?`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=``,Yt(i.account?.email??t),gt(),Qt()}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=``,Yt(null),F.close(),y=-1,o=`local`,h=`idle`,g=``,n.remoteUrl.value=``,n.sharePanel.hidden=!0,n.publishPanel.hidden=!0,n.adminPanel.hidden=!0,n.onairPanel.hidden=!0,n.catalogsPanel.hidden=!0,n.listenOnly.hidden=!0,nn(!1);try{localStorage.removeItem(De)}catch{}_=`Signed out, and disconnected from the server.`,gt(),B()})()});try{let e=new URL(globalThis.location.href).searchParams,t=e.get(`url`)??``;t!==``&&(Te=t,l=e.get(`play`)??``,u=Math.max(0,Number(e.get(`t`)??`0`)||0),n.remoteUrl.value=t,_=`Opening the stream you were sent…`,globalThis.history?.replaceState(null,``,globalThis.location.pathname))}catch{}Xt(),Zt(),gt(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}ct(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{F.close(),n.listenOnly.hidden=!0,y=-1,n.sharePanel.hidden=!0,n.publishPanel.hidden=!0,n.adminPanel.hidden=!0,n.onairPanel.hidden=!0,n.onairPanel.dataset.title=`Live on this server`,n.catalogsPanel.hidden=!0,n.catalogsPanel.dataset.title=`Catalogs on this server`,s=``,d=``,Ct(),nn(!1),o=`local`,h=`idle`,g=``,B()});async function $t(){if(o!==`remote`||F.shareLink===``){n.sharePanel.hidden=!0;return}n.sharePanel.hidden=!1;let e=mn(),t=globalThis.location.origin;n.shareLink.value=e===``?``: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 r=``;try{let e=await fetch(`/api/directory`);e.ok&&(r=(await e.json()).callIn??``)}catch{}let i=null;try{let r=await fetch(F.url(`/api/live/state`));r.ok&&(i=await r.json()),i?.url&&(e=i.url,d=i.url,n.shareLink.value=e.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e)}`:e)}catch{}if(i){if(Se=i.live,O=i.live?i.code:``,k=r,n.liveControls.hidden=n.adminPanel.hidden||!i.possible,n.goLive.hidden=i.live,n.stopLive.hidden=!i.live,n.sharePhone.hidden=!1,!i.live){n.sharePhone.textContent=i.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(!r){n.sharePhone.textContent=`Listed. The code for the phone line is ${i.code}.`,n.shareSend.hidden=!1;return}n.sharePhone.replaceChildren(document.createTextNode(`To talk about it, call `),bn(r),document.createTextNode(` and key `),bn(i.code),document.createTextNode(`. That is a room with everyone else watching — not the stream itself.`)),n.shareSend.hidden=!1}}let en=``,tn=null,nn=e=>{tn&&clearInterval(tn),tn=null,e&&(tn=setInterval(()=>void $(),6e3))};async function $(){if(o!==`remote`){n.onairPanel.hidden=!0;return}let e;try{let t=await fetch(F.url(`/api/streams`));if(!t.ok){n.onairPanel.hidden=!0;return}e=await t.json(),e.server.name&&e.server.name!==s&&(s=e.server.name,n.onairPanel.dataset.title=`Live on ${s}`,n.catalogsPanel.dataset.title=`Catalogs on ${s}`,Ct(),B())}catch{n.onairPanel.hidden=!0;return}n.onairPanel.hidden=!1,C=e,hn(e);let t=`${n.adminPanel.hidden?`view`:`drive`}:${JSON.stringify(e)}`;if(t===en)return;en=t;let r=e.restreams??[],i=e.channels.length+r.length;n.onairNote.textContent=i===0?`One stream, from this server's own files.`:`${i+1} streams: this server's own files, and ${i} more on it.`;let a=[],c=e.server.playing,l=!n.adminPanel.hidden;a.push(gn({title:e.server.name,detail:[c?`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:c?`Join live`:l?`Start the stream`:`Nothing playing`,onPlay:()=>{if(c){on(e.server.nowPlaying);return}l&&an()},link:e.server.live?e.server.url:``,...c?{page:pn(`live`)}:{},direct:c?F.url(`/api/live`):``}));for(let e of r)a.push(gn({title:e.name,detail:e.tracks===1?`re-streamed from the web`:`re-streamed from the web · ${e.tracks} tracks`,onPlay:()=>{R(e.at)},link:``,direct:F.media(e.at)}));for(let t of e.channels){let e=t.kind!==`audio`,n=F.url(`/api/channels/${encodeURIComponent(t.id)}`),r=[t.via===`pull`?`on the air · ${t.listeners} watching`:`live over ${t.via} · ${t.listeners} listening`];t.code&&r.push(`☎ ${t.code}`),t.redials&&r.push(`redialled ${t.redials}×`),l&&t.error&&r.push(t.error),a.push(gn({title:t.name,detail:r.join(` · `),onPlay:()=>{sn({id:t.id,name:t.name,video:e})},link:n,page:pn(`channel:${t.id}`),direct:n,onRestart:l&&t.via===`pull`?()=>{un(t.id,t.name)}:void 0,onStop:l?()=>{dn(t.id,t.name)}:void 0}))}n.onairList.replaceChildren(...a)}async function rn(){if(o===`remote`)try{if((await fetch(F.media(L(),0),{method:`GET`,headers:{range:`bytes=0-1`}})).status!==402)return;_=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`}),B()}catch{}}async function an(){U(`Starting the stream on the server…`);try{await F.send({type:`play`,index:Math.max(0,L())})}catch{U(`could not reach the server`);return}await on(m.tracks[L()]?.title??``),U(`Playing to the room. Anybody with the view link sees this.`),await $()}async function on(e){y=-1,b=null,S={kind:`live`},D(e,`auto`),await x(()=>P.load({title:e||`Live`,artist:``,album:``,duration:0,url:F.url(`/api/live`),video:!0,objectUrl:!1},!0)),it(!0),_=`Watching what this server is playing. Everyone here sees the same thing.`,B()}async function sn(e,t=!0,n){y=-1,b=e,t&&(ne=0),t&&(S=n??{kind:`channel`}),t&&D(e.name,S?.link||Ee(e.name)?`auto`:`channel`);let r=e.video&&Ne();await x(()=>P.load({title:e.name,artist:``,album:``,duration:0,url:F.url(r?`/api/channels/${encodeURIComponent(e.id)}/hls/index.m3u8`:`/api/channels/${encodeURIComponent(e.id)}`),video:e.video,objectUrl:!1},!0)),it(e.video),b===e&&(_=`Watching ${e.name}, live on this server.`),B()}function cn(){let e=b;return e?we?!0:ne>=5?(_=`${e.name} stopped, and did not come back.`,b=null,S=null,B(),!0):(ne+=1,_=`${e.name} started over; rejoining…`,B(),we=setTimeout(()=>{we=null,b===e&&sn(e,!1)},2e3),!0):!1}function ln(e){n.onairNote.textContent=e,U(e)}async function un(e,t){ln(`Restarting ${t}…`);try{let n=await fetch(F.url(`/api/channels/${encodeURIComponent(e)}/restart`),{method:`POST`}),r=await n.json().catch(()=>({}));ln(n.ok?`${t} is dialling its source again.`:r.error??`that did not work`)}catch{ln(`could not reach the server`)}en=``,$()}async function dn(e,t){ln(`Taking ${t} off the air…`);try{let n=await fetch(F.url(`/api/channels/${encodeURIComponent(e)}`),{method:`DELETE`}),r=await n.json().catch(()=>({}));ln(n.ok?`${t} is off the air.`:r.error??`that did not work`)}catch{ln(`could not reach the server`)}b?.id===e&&(b=null,P.stop()),en=``,$()}async function fn(e,t,n=`Copied`){if(!e)return;let r=t.innerHTML;try{await navigator.clipboard.writeText(e)}catch{_=e,B();return}n===`✓`||n===`✓`?i(t,`check`):t.textContent=n,setTimeout(()=>{t.innerHTML=r},1200)}function pn(e,t=0){let n=mn();if(n===``)return``;let r=globalThis.location.origin,i=t>1?`&t=${Math.floor(t)}`:``;return`${r}/?url=${encodeURIComponent(n)}&play=${encodeURIComponent(e)}${i}`}function mn(){if(d!==``)return d;let e=o===`remote`?F.shareLink:``;return/\/admin\//.test(e)?``:e}function hn(e){if(l===``)return;let t=l;if(t===`live`){l=``,e.server.playing?on(e.server.nowPlaying):(_=`Nothing is playing on this server right now.`,B());return}if(t.startsWith(`link:`)){l=``,ot(t.slice(5));return}let n=t.startsWith(`channel:`)?t.slice(8):``,r=e.channels.find(e=>e.id===n)??e.channels.find(e=>e.name===n);r&&(l=``,sn({id:r.id,name:r.name,video:r.kind!==`audio`}))}function gn(e){let t=document.createElement(`li`);t.className=`onair`;let n=document.createElement(`span`);n.className=`recent-label`;let r=document.createElement(`span`);r.className=`name`,r.textContent=e.title;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.detail,n.append(r,a);let o=document.createElement(`span`);o.className=`onair-actions`;let s=document.createElement(`button`);s.type=`button`,s.className=`button`,s.textContent=e.playLabel??`Play`,s.addEventListener(`click`,e.onPlay),o.append(s);let c=(e,t,n)=>{let r=document.createElement(`button`);return r.type=`button`,r.className=`icon`,i(r,e),r.title=t,r.setAttribute(`aria-label`,t),r.addEventListener(`click`,()=>n(r)),r};return(e.link||e.page)&&o.append(c(`link`,`Copy a link that opens this in the player`,t=>{let n=globalThis.location.origin;fn(e.page??(e.link.startsWith(`https://`)?`${n}/?url=${encodeURIComponent(e.link)}`:e.link),t,`✓`)})),e.direct&&o.append(c(`copy`,`Copy the stream's own URL, for VLC or mpv`,t=>{fn(e.direct??``,t,`✓`)})),e.onRestart&&o.append(c(`restart`,`Restart: dial the source again`,()=>e.onRestart?.())),e.onStop&&o.append(c(`remove`,`Remove: take it off the air`,()=>e.onStop?.())),t.append(n,o),t}let _n=``;function vn(e,t){_n=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`,()=>{_n!==``&&(n.loadHome.disabled=!0,U(`Reading this server's files…`),(async()=>{try{let e=await fetch(F.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:_n})}),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 yn=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(F.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 $t()}};n.goLive.addEventListener(`click`,()=>void yn(!0)),n.stopLive.addEventListener(`click`,()=>void yn(!1));function bn(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:mn()})}),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(ke,n.listenHere.checked?`1`:`0`)}catch{}o===`remote`&&(async()=>{n.listenHere.checked?(await F.send({type:`stop`}),await Ue(m.index)):(P.stop(),y=-1),B()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),We();return;case`s`:Ge();return;case`n`:case`ArrowRight`:z(1);return;case`p`:case`ArrowLeft`:z(-1);return;case`ArrowDown`:e.preventDefault(),R(Math.min(I()-1,L()+1));return;case`ArrowUp`:e.preventDefault(),R(Math.max(0,L()-1));return}});let xn=null;globalThis.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),xn=e,n.install.hidden=!1}),n.install.addEventListener(`click`,()=>{xn?.prompt(),xn=null,n.install.hidden=!0});try{let e=localStorage.getItem(Oe);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),P.volume=Number(e));let t=localStorage.getItem(De);t&&(n.remoteUrl.value=t),localStorage.getItem(ke)===`0`&&(n.listenHere.checked=!1)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await me(e)===null)return;let t=await de(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,o=`remote`,_=``,F.connect(e),B())})(),(()=>{if(Ae||Te!==``)return;let e=()=>P.source!==``||P.playing||ce()||b!==null,t=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``},n=new Audio;n.volume=.7;let r=()=>{Ae=!0},i=()=>{document.removeEventListener(`pointerdown`,i),document.removeEventListener(`keydown`,i),r(),setTimeout(()=>{e()||n.play().catch(()=>{})},150)};t().then(t=>{if(!(t===``||e()))return n.src=t,n.play().then(r,()=>{document.addEventListener(`pointerdown`,i,{once:!0}),document.addEventListener(`keydown`,i,{once:!0})})})})(),B(),requestAnimationFrame(rt)}M(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};