nixamp 0.11.2 → 0.12.1

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. */
@@ -71,8 +73,13 @@ export declare function whereToAsk(kind: EnrichKind, parsedKind?: string): {
71
73
  } | null;
72
74
  /** The key one name is remembered under: case and spacing do not make it a different name. */
73
75
  export declare function cacheKey(name: string, kind: EnrichKind, year: number | null): string;
74
- /** Whether a stored answer is still worth believing. */
75
- export declare function fresh(entry: Cached, now: number): boolean;
76
+ /**
77
+ * Whether a stored answer is still worth believing. A miss on a name that
78
+ * reads as a game is believed only as long as a score would be: the fixture
79
+ * may not be listed yet, and a channel named for tonight's game is asked
80
+ * about again while it plays, not six hours from now.
81
+ */
82
+ export declare function fresh(entry: Cached, now: number, matchup?: boolean): boolean;
76
83
  /** The best of nichedb's answers, or nothing when the best is a guess. */
77
84
  export declare function pickBest(answer: MatchAnswer, asked: string): Enriched | null;
78
85
  export interface EnricherOptions {
@@ -111,4 +118,3 @@ export declare class Enricher {
111
118
  /** Write the cache now. Called on a timer, and by whoever is shutting down. */
112
119
  save(): void;
113
120
  }
114
- 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. */
@@ -61,9 +63,20 @@ export function whereToAsk(kind, parsedKind) {
61
63
  export function cacheKey(name, kind, year) {
62
64
  return `${kind}|${year ?? ""}|${name.trim().toLowerCase().replace(/\s+/g, " ")}`;
63
65
  }
64
- /** Whether a stored answer is still worth believing. */
65
- export function fresh(entry, now) {
66
- const ttl = entry.hit === null ? MISS_TTL_MS : entry.hit.kind === "fixture" ? FIXTURE_TTL_MS : HIT_TTL_MS;
66
+ /**
67
+ * Whether a stored answer is still worth believing. A miss on a name that
68
+ * reads as a game is believed only as long as a score would be: the fixture
69
+ * may not be listed yet, and a channel named for tonight's game is asked
70
+ * about again while it plays, not six hours from now.
71
+ */
72
+ export function fresh(entry, now, matchup = false) {
73
+ const ttl = entry.hit === null
74
+ ? matchup
75
+ ? FIXTURE_TTL_MS
76
+ : MISS_TTL_MS
77
+ : entry.hit.kind === "fixture"
78
+ ? FIXTURE_TTL_MS
79
+ : HIT_TTL_MS;
67
80
  return now - entry.at < ttl;
68
81
  }
69
82
  /** The best of nichedb's answers, or nothing when the best is a guess. */
@@ -74,6 +87,14 @@ export function pickBest(answer, asked) {
74
87
  const isExact = (item) => item !== undefined && String(item.title ?? "").toLowerCase() === wanted;
75
88
  const plain = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
76
89
  const askedPlain = plain(wanted);
90
+ // nichedb keeps every meeting of two teams under the same title: the one
91
+ // being played now outranks the one next month, which outranks last year's.
92
+ const stateRank = (item) => {
93
+ if (item?.kind !== "fixture")
94
+ return 1;
95
+ const state = String(item.data?.["state"] ?? item.tags?.find((t) => t.startsWith("state:"))?.slice(6) ?? "");
96
+ return state === "in" ? 0 : state === "pre" ? 1 : state === "post" ? 2 : 1;
97
+ };
77
98
  for (const item of items) {
78
99
  const exact = isExact(item);
79
100
  const score = Number(item.score ?? 0);
@@ -92,10 +113,14 @@ export function pickBest(answer, asked) {
92
113
  best = item;
93
114
  else if (exact && !isExact(best))
94
115
  best = item;
95
- else if (exact && isExact(best) && !best.image_url && item.image_url)
116
+ else if (exact && isExact(best) && stateRank(item) < stateRank(best))
117
+ best = item;
118
+ else if (exact && isExact(best) && stateRank(item) === stateRank(best) && !best.image_url && item.image_url)
96
119
  best = item;
97
120
  else if (!isExact(best) && score > Number(best.score ?? 0))
98
121
  best = item;
122
+ else if (!isExact(best) && score === Number(best.score ?? 0) && stateRank(item) < stateRank(best))
123
+ best = item;
99
124
  }
100
125
  if (!best)
101
126
  return null;
@@ -150,7 +175,7 @@ export class Enricher {
150
175
  return null;
151
176
  const key = cacheKey(asked, kind, year);
152
177
  const had = this.cache.get(key);
153
- if (had && fresh(had, this.now()))
178
+ if (had && fresh(had, this.now(), isMatchupName(asked)))
154
179
  return had.hit;
155
180
  const running = this.inflight.get(key);
156
181
  if (running)
@@ -180,10 +205,35 @@ export class Enricher {
180
205
  first.set("collection", where.collection);
181
206
  first.set("kind", where.kind);
182
207
  }
208
+ // Two sides with "vs" or "@" between them are a fixture until nichedb's
209
+ // sports collection says it has none: nichedb's parser reads a name, and
210
+ // "Chiefs vs Bills" reads as a title to a parser that expects films.
211
+ if (kind === "auto" && isMatchupName(name)) {
212
+ const fixture = new URLSearchParams(first);
213
+ fixture.set("collection", "sports");
214
+ fixture.set("kind", "fixture");
215
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
216
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE)
217
+ return hit;
218
+ }
183
219
  const answer = await this.get(`/api/v1/match?${first}`);
184
220
  if (where)
185
221
  return pickBest(answer, answer.parsed?.name ?? name);
186
- const guessed = whereToAsk("auto", answer.parsed?.kind);
222
+ // nichedb reads "Alien vs Predator" as a game too. Once the sports
223
+ // collection has said it has none, the name is a title after all.
224
+ let parsedKind = answer.parsed?.kind;
225
+ if (parsedKind === "fixture") {
226
+ if (!isMatchupName(name)) {
227
+ const fixture = new URLSearchParams(first);
228
+ fixture.set("collection", "sports");
229
+ fixture.set("kind", "fixture");
230
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
231
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE)
232
+ return hit;
233
+ }
234
+ parsedKind = "title";
235
+ }
236
+ const guessed = whereToAsk("auto", parsedKind);
187
237
  if (!guessed)
188
238
  return null;
189
239
  const second = new URLSearchParams(first);
@@ -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";
@@ -2342,8 +2342,12 @@ export function createHandler(engine, options) {
2342
2342
  ...CORS,
2343
2343
  "content-type": "application/json; charset=utf-8",
2344
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.
2346
- "cache-control": match ? "public, max-age=300" : "public, max-age=120",
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",
2347
2351
  });
2348
2352
  response.end(JSON.stringify({ match }));
2349
2353
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
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";
@@ -105,9 +108,21 @@ export function cacheKey(name: string, kind: EnrichKind, year: number | null): s
105
108
  return `${kind}|${year ?? ""}|${name.trim().toLowerCase().replace(/\s+/g, " ")}`;
106
109
  }
107
110
 
108
- /** Whether a stored answer is still worth believing. */
109
- export function fresh(entry: Cached, now: number): boolean {
110
- const ttl = entry.hit === null ? MISS_TTL_MS : entry.hit.kind === "fixture" ? FIXTURE_TTL_MS : HIT_TTL_MS;
111
+ /**
112
+ * Whether a stored answer is still worth believing. A miss on a name that
113
+ * reads as a game is believed only as long as a score would be: the fixture
114
+ * may not be listed yet, and a channel named for tonight's game is asked
115
+ * about again while it plays, not six hours from now.
116
+ */
117
+ export function fresh(entry: Cached, now: number, matchup = false): boolean {
118
+ const ttl =
119
+ entry.hit === null
120
+ ? matchup
121
+ ? FIXTURE_TTL_MS
122
+ : MISS_TTL_MS
123
+ : entry.hit.kind === "fixture"
124
+ ? FIXTURE_TTL_MS
125
+ : HIT_TTL_MS;
111
126
  return now - entry.at < ttl;
112
127
  }
113
128
 
@@ -121,6 +136,13 @@ export function pickBest(answer: MatchAnswer, asked: string): Enriched | null {
121
136
  item !== undefined && String(item.title ?? "").toLowerCase() === wanted;
122
137
  const plain = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
123
138
  const askedPlain = plain(wanted);
139
+ // nichedb keeps every meeting of two teams under the same title: the one
140
+ // being played now outranks the one next month, which outranks last year's.
141
+ const stateRank = (item: Item | undefined): number => {
142
+ if (item?.kind !== "fixture") return 1;
143
+ const state = String(item.data?.["state"] ?? item.tags?.find((t) => t.startsWith("state:"))?.slice(6) ?? "");
144
+ return state === "in" ? 0 : state === "pre" ? 1 : state === "post" ? 2 : 1;
145
+ };
124
146
  for (const item of items) {
125
147
  const exact = isExact(item);
126
148
  const score = Number(item.score ?? 0);
@@ -137,8 +159,10 @@ export function pickBest(answer: MatchAnswer, asked: string): Enriched | null {
137
159
  // a film and only one has the poster; among the rest, the score decides.
138
160
  if (!best) best = item;
139
161
  else if (exact && !isExact(best)) best = item;
140
- else if (exact && isExact(best) && !best.image_url && item.image_url) best = item;
162
+ else if (exact && isExact(best) && stateRank(item) < stateRank(best)) best = item;
163
+ else if (exact && isExact(best) && stateRank(item) === stateRank(best) && !best.image_url && item.image_url) best = item;
141
164
  else if (!isExact(best) && score > Number(best.score ?? 0)) best = item;
165
+ else if (!isExact(best) && score === Number(best.score ?? 0) && stateRank(item) < stateRank(best)) best = item;
142
166
  }
143
167
  if (!best) return null;
144
168
  const kind = best.kind === "channel" || best.kind === "fixture" ? best.kind : "title";
@@ -202,7 +226,7 @@ export class Enricher {
202
226
  if (asked === "") return null;
203
227
  const key = cacheKey(asked, kind, year);
204
228
  const had = this.cache.get(key);
205
- if (had && fresh(had, this.now())) return had.hit;
229
+ if (had && fresh(had, this.now(), isMatchupName(asked))) return had.hit;
206
230
  const running = this.inflight.get(key);
207
231
  if (running) return running;
208
232
  const work = this.ask(asked, kind, year)
@@ -230,9 +254,32 @@ export class Enricher {
230
254
  first.set("collection", where.collection);
231
255
  first.set("kind", where.kind);
232
256
  }
257
+ // Two sides with "vs" or "@" between them are a fixture until nichedb's
258
+ // sports collection says it has none: nichedb's parser reads a name, and
259
+ // "Chiefs vs Bills" reads as a title to a parser that expects films.
260
+ if (kind === "auto" && isMatchupName(name)) {
261
+ const fixture = new URLSearchParams(first);
262
+ fixture.set("collection", "sports");
263
+ fixture.set("kind", "fixture");
264
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
265
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE) return hit;
266
+ }
233
267
  const answer = await this.get(`/api/v1/match?${first}`);
234
268
  if (where) return pickBest(answer, answer.parsed?.name ?? name);
235
- const guessed = whereToAsk("auto", answer.parsed?.kind);
269
+ // nichedb reads "Alien vs Predator" as a game too. Once the sports
270
+ // collection has said it has none, the name is a title after all.
271
+ let parsedKind = answer.parsed?.kind;
272
+ if (parsedKind === "fixture") {
273
+ if (!isMatchupName(name)) {
274
+ const fixture = new URLSearchParams(first);
275
+ fixture.set("collection", "sports");
276
+ fixture.set("kind", "fixture");
277
+ const hit = pickBest(await this.get(`/api/v1/match?${fixture}`), name);
278
+ if (hit && hit.kind === "fixture" && hit.score >= MIN_SCORE) return hit;
279
+ }
280
+ parsedKind = "title";
281
+ }
282
+ const guessed = whereToAsk("auto", parsedKind);
236
283
  if (!guessed) return null;
237
284
  const second = new URLSearchParams(first);
238
285
  second.set("collection", guessed.collection);
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,
@@ -2816,8 +2816,12 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2816
2816
  ...CORS,
2817
2817
  "content-type": "application/json; charset=utf-8",
2818
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.
2820
- "cache-control": match ? "public, max-age=300" : "public, max-age=120",
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",
2821
2825
  });
2822
2826
  response.end(JSON.stringify({ match }));
2823
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};