nixamp 0.10.3 → 0.11.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/src/enrich.ts ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * What is this, really?
3
+ *
4
+ * A file called "Top.Gun.Maverick.2022.1080p.WEB-DL.mkv" is a film with a
5
+ * poster, a year and a rating; a playlist entry called "US: ESPN2 HD" is a
6
+ * channel with a logo, a country and a category; "Lakers at Celtics" is a
7
+ * fixture with a score. nixamp knows none of that on its own -- ffprobe reads
8
+ * tags, and a torrent's tags are its file name -- so it asks nichedb.dev,
9
+ * which keeps the titles, channels and fixtures every profullstack site is
10
+ * built on, and answers a name with the best match and a score.
11
+ *
12
+ * Asked once per name and remembered: a library of five thousand files must
13
+ * not become five thousand requests a day, and a channel that was ESPN2
14
+ * yesterday is ESPN2 today. Misses are remembered too, for less long, so a
15
+ * file nichedb has never heard of is not asked about every time it plays.
16
+ */
17
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
18
+ import { dirname } from "node:path";
19
+
20
+ /** Where the answers come from, unless a deployment says otherwise. */
21
+ export const DEFAULT_SITE = "https://nichedb.dev";
22
+ /** How long a hit is believed. Titles and channels change on the order of months. */
23
+ export const HIT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
24
+ /** How long a miss is believed: nichedb's catalogue is still filling in. */
25
+ export const MISS_TTL_MS = 6 * 60 * 60 * 1000;
26
+ /** A fixture's score is stale in a minute. */
27
+ export const FIXTURE_TTL_MS = 60 * 1000;
28
+ /**
29
+ * Below this the best match is a guess, and a wrong poster is worse than none.
30
+ * Measured: "Severance" against a channel called "Sever" scored 0.45, and
31
+ * "Lakers at Celtics" against "Rangers at Celtic" 0.44. Half is above both.
32
+ */
33
+ export const MIN_SCORE = 0.5;
34
+ /** A weaker score is still taken when one name plainly begins with the other. */
35
+ export const PREFIX_SCORE = 0.42;
36
+ /** How many answers the cache keeps before the oldest go. */
37
+ export const MAX_ENTRIES = 5000;
38
+
39
+ export type EnrichKind = "auto" | "title" | "channel" | "fixture";
40
+
41
+ export interface Enriched {
42
+ /** What nichedb says it is. */
43
+ kind: "title" | "channel" | "fixture";
44
+ title: string;
45
+ /** For a title, its year; for a fixture, when it starts. */
46
+ year: number | null;
47
+ /** A poster, a logo, or nothing. */
48
+ image: string | null;
49
+ summary: string | null;
50
+ /** nichedb's page for it, for a link. */
51
+ page: string;
52
+ score: number;
53
+ /** The rest, as the collection shapes it: rating, genres, country, scores… */
54
+ data: Record<string, unknown>;
55
+ tags: string[];
56
+ }
57
+
58
+ interface Cached {
59
+ at: number;
60
+ hit: Enriched | null;
61
+ }
62
+
63
+ /** nichedb's answer to /api/v1/match, as much of it as is read here. */
64
+ interface MatchAnswer {
65
+ parsed?: { name?: string; year?: number | null; kind?: string; season?: number | null; episode?: number | null };
66
+ items?: {
67
+ kind?: string;
68
+ title?: string;
69
+ summary?: string | null;
70
+ image_url?: string | null;
71
+ published_at?: string | null;
72
+ page?: string;
73
+ score?: number;
74
+ data?: Record<string, unknown>;
75
+ tags?: string[];
76
+ }[];
77
+ }
78
+
79
+ /** The collection and kind a name is asked about, from what the caller knows. */
80
+ export function whereToAsk(kind: EnrichKind, parsedKind?: string): { collection: string; kind: string } | null {
81
+ const k = kind === "auto" ? parsedKind ?? "" : kind;
82
+ switch (k) {
83
+ case "channel":
84
+ return { collection: "channels", kind: "channel" };
85
+ case "fixture":
86
+ return { collection: "sports", kind: "fixture" };
87
+ case "title":
88
+ case "movie":
89
+ case "series":
90
+ return { collection: "screen", kind: "title" };
91
+ default:
92
+ // Music and the rest: nichedb has no answer worth a poster yet.
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /** The key one name is remembered under: case and spacing do not make it a different name. */
98
+ export function cacheKey(name: string, kind: EnrichKind, year: number | null): string {
99
+ return `${kind}|${year ?? ""}|${name.trim().toLowerCase().replace(/\s+/g, " ")}`;
100
+ }
101
+
102
+ /** Whether a stored answer is still worth believing. */
103
+ export function fresh(entry: Cached, now: number): boolean {
104
+ const ttl = entry.hit === null ? MISS_TTL_MS : entry.hit.kind === "fixture" ? FIXTURE_TTL_MS : HIT_TTL_MS;
105
+ return now - entry.at < ttl;
106
+ }
107
+
108
+ /** The best of nichedb's answers, or nothing when the best is a guess. */
109
+ export function pickBest(answer: MatchAnswer, asked: string): Enriched | null {
110
+ const items = answer.items ?? [];
111
+ const wanted = asked.trim().toLowerCase();
112
+ type Item = NonNullable<MatchAnswer["items"]>[number];
113
+ let best: Item | undefined;
114
+ const isExact = (item: Item | undefined): boolean =>
115
+ item !== undefined && String(item.title ?? "").toLowerCase() === wanted;
116
+ const plain = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
117
+ const askedPlain = plain(wanted);
118
+ for (const item of items) {
119
+ const exact = isExact(item);
120
+ const score = Number(item.score ?? 0);
121
+ const titlePlain = plain(String(item.title ?? ""));
122
+ // Whole words: "Top Gun Maverick Extended" begins with "Top Gun Maverick",
123
+ // but "Severance" does not begin with a channel called "Sever".
124
+ const prefix =
125
+ titlePlain.length >= 4 &&
126
+ askedPlain.length >= 4 &&
127
+ (askedPlain.startsWith(`${titlePlain} `) || titlePlain.startsWith(`${askedPlain} `));
128
+ if (!exact && score < (prefix ? PREFIX_SCORE : MIN_SCORE)) continue;
129
+ // An exact title beats any score; among exact titles the one with a
130
+ // picture wins, since nichedb keeps both the IMDb row and the TMDB row of
131
+ // a film and only one has the poster; among the rest, the score decides.
132
+ if (!best) best = item;
133
+ else if (exact && !isExact(best)) best = item;
134
+ else if (exact && isExact(best) && !best.image_url && item.image_url) best = item;
135
+ else if (!isExact(best) && score > Number(best.score ?? 0)) best = item;
136
+ }
137
+ if (!best) return null;
138
+ const kind = best.kind === "channel" || best.kind === "fixture" ? best.kind : "title";
139
+ const year = typeof best.data?.["year"] === "number"
140
+ ? (best.data["year"] as number)
141
+ : best.published_at
142
+ ? new Date(best.published_at).getUTCFullYear() || null
143
+ : null;
144
+ return {
145
+ kind,
146
+ title: String(best.title ?? ""),
147
+ year: Number.isFinite(year) ? year : null,
148
+ image: best.image_url ?? null,
149
+ summary: best.summary ?? null,
150
+ page: String(best.page ?? ""),
151
+ score: Number(best.score ?? 0),
152
+ data: best.data ?? {},
153
+ tags: best.tags ?? [],
154
+ };
155
+ }
156
+
157
+ export interface EnricherOptions {
158
+ site?: string;
159
+ fetch?: typeof globalThis.fetch;
160
+ /** Where answers are kept between runs; none means memory only. */
161
+ cacheFile?: string;
162
+ now?: () => number;
163
+ onEvent?: (message: string) => void;
164
+ }
165
+
166
+ export class Enricher {
167
+ private readonly site: string;
168
+ private readonly fetcher: typeof globalThis.fetch;
169
+ private readonly now: () => number;
170
+ private readonly cache = new Map<string, Cached>();
171
+ private readonly inflight = new Map<string, Promise<Enriched | null>>();
172
+ private saveTimer: ReturnType<typeof setTimeout> | null = null;
173
+ private dirty = false;
174
+
175
+ constructor(private readonly options: EnricherOptions = {}) {
176
+ this.site = (options.site ?? DEFAULT_SITE).replace(/\/+$/, "");
177
+ this.fetcher = options.fetch ?? globalThis.fetch;
178
+ this.now = options.now ?? Date.now;
179
+ this.load();
180
+ }
181
+
182
+ /** How many names are remembered. */
183
+ get size(): number {
184
+ return this.cache.size;
185
+ }
186
+
187
+ /**
188
+ * What a name is, from the cache or from nichedb.
189
+ *
190
+ * `kind` narrows the question when the caller knows: a live channel is a
191
+ * channel however its name reads. `auto` lets nichedb's parser decide from
192
+ * the name itself, which is right for a file.
193
+ */
194
+ async lookup(name: string, kind: EnrichKind = "auto", year: number | null = null): Promise<Enriched | null> {
195
+ const asked = String(name ?? "").trim();
196
+ if (asked === "") return null;
197
+ const key = cacheKey(asked, kind, year);
198
+ const had = this.cache.get(key);
199
+ if (had && fresh(had, this.now())) return had.hit;
200
+ const running = this.inflight.get(key);
201
+ if (running) return running;
202
+ const work = this.ask(asked, kind, year)
203
+ .then((hit) => {
204
+ this.remember(key, hit);
205
+ return hit;
206
+ })
207
+ .catch((error: unknown) => {
208
+ this.options.onEvent?.(` nichedb did not answer for "${asked}": ${(error as Error).message}`);
209
+ // Not remembered: a network fault is not a miss.
210
+ return had?.hit ?? null;
211
+ })
212
+ .finally(() => this.inflight.delete(key));
213
+ this.inflight.set(key, work);
214
+ return work;
215
+ }
216
+
217
+ private async ask(name: string, kind: EnrichKind, year: number | null): Promise<Enriched | null> {
218
+ // Two round trips at most: nichedb parses the name; when the caller did
219
+ // not say what it is, the first answer's reading says where to look.
220
+ const first = new URLSearchParams({ q: name, limit: "3" });
221
+ if (year !== null) first.set("year", String(year));
222
+ const where = whereToAsk(kind);
223
+ if (where) {
224
+ first.set("collection", where.collection);
225
+ first.set("kind", where.kind);
226
+ }
227
+ const answer = await this.get(`/api/v1/match?${first}`);
228
+ if (where) return pickBest(answer, answer.parsed?.name ?? name);
229
+ const guessed = whereToAsk("auto", answer.parsed?.kind);
230
+ if (!guessed) return null;
231
+ const second = new URLSearchParams(first);
232
+ second.set("collection", guessed.collection);
233
+ second.set("kind", guessed.kind);
234
+ // The year the name carried narrows the second question.
235
+ if (year === null && answer.parsed?.year) second.set("year", String(answer.parsed.year));
236
+ return pickBest(await this.get(`/api/v1/match?${second}`), answer.parsed?.name ?? name);
237
+ }
238
+
239
+ private async get(path: string): Promise<MatchAnswer> {
240
+ const response = await this.fetcher(`${this.site}${path}`, {
241
+ headers: { accept: "application/json", "user-agent": "nixamp (+https://nixamp.com)" },
242
+ signal: AbortSignal.timeout(15_000),
243
+ });
244
+ if (!response.ok) throw new Error(`nichedb answered ${response.status}`);
245
+ return (await response.json()) as MatchAnswer;
246
+ }
247
+
248
+ private remember(key: string, hit: Enriched | null): void {
249
+ this.cache.set(key, { at: this.now(), hit });
250
+ if (this.cache.size > MAX_ENTRIES) {
251
+ // Oldest first: a Map remembers insertion order.
252
+ const drop = this.cache.size - MAX_ENTRIES;
253
+ let n = 0;
254
+ for (const k of this.cache.keys()) {
255
+ if (n++ >= drop) break;
256
+ this.cache.delete(k);
257
+ }
258
+ }
259
+ this.dirty = true;
260
+ this.scheduleSave();
261
+ }
262
+
263
+ private load(): void {
264
+ if (!this.options.cacheFile) return;
265
+ try {
266
+ const parsed = JSON.parse(readFileSync(this.options.cacheFile, "utf8")) as Record<string, Cached>;
267
+ for (const [k, v] of Object.entries(parsed)) {
268
+ if (v && typeof v.at === "number") this.cache.set(k, v);
269
+ }
270
+ } catch {
271
+ // No cache yet, or one that is not JSON: start empty.
272
+ }
273
+ }
274
+
275
+ private scheduleSave(): void {
276
+ if (!this.options.cacheFile || this.saveTimer) return;
277
+ this.saveTimer = setTimeout(() => {
278
+ this.saveTimer = null;
279
+ this.save();
280
+ }, 2000);
281
+ this.saveTimer.unref?.();
282
+ }
283
+
284
+ /** Write the cache now. Called on a timer, and by whoever is shutting down. */
285
+ save(): void {
286
+ if (!this.options.cacheFile || !this.dirty) return;
287
+ try {
288
+ mkdirSync(dirname(this.options.cacheFile), { recursive: true });
289
+ const tmp = `${this.options.cacheFile}.tmp`;
290
+ writeFileSync(tmp, JSON.stringify(Object.fromEntries(this.cache)));
291
+ renameSync(tmp, this.options.cacheFile);
292
+ this.dirty = false;
293
+ } catch (error) {
294
+ this.options.onEvent?.(` could not save the enrichment cache: ${(error as Error).message}`);
295
+ }
296
+ }
297
+ }
package/src/server.ts CHANGED
@@ -52,6 +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
56
  import {
56
57
  contentTypeFor, downloadArgs, fileNameFor, inputArgsFor, linkChannelId, playableLink, resolveLink, saveFormat,
57
58
  type ResolvedLink,
@@ -1166,6 +1167,8 @@ export interface HandlerOptions {
1166
1167
  ytdlp?: string[] | null;
1167
1168
  /** Channels as HLS, for Safari on a phone, which plays a live stream no other way. */
1168
1169
  hls?: HlsPackagers;
1170
+ /** What a name is -- a film, a channel, a fixture -- asked of nichedb.dev and remembered. */
1171
+ enricher?: Enricher;
1169
1172
  /** A Netscape cookies file for sites that want a signed-in browser, when there is one. */
1170
1173
  cookies?: string;
1171
1174
  /** Who is listening, for the admin view. */
@@ -2789,6 +2792,36 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2789
2792
  // own, the way a catalog entry is played: started for whoever asked,
2790
2793
  // stopped a minute after the last viewer leaves. Open to anyone holding
2791
2794
  // the link, like picking something from a catalog.
2795
+ // --- what is this? ------------------------------------------------------
2796
+ //
2797
+ // A file name, a playlist entry, a channel: the poster, the logo, the
2798
+ // year, the rating, from nichedb.dev, remembered here so a library is
2799
+ // asked about once. Open to whoever holds the link, like the playlist.
2800
+ if (path === "/api/enrich" && request.method === "GET") {
2801
+ const name = (url.searchParams.get("name") ?? "").trim().slice(0, 300);
2802
+ if (name === "") {
2803
+ json(response, 400, { error: "name is required" });
2804
+ return;
2805
+ }
2806
+ if (!options.enricher) {
2807
+ json(response, 200, { match: null });
2808
+ return;
2809
+ }
2810
+ const kinds: EnrichKind[] = ["auto", "title", "channel", "fixture"];
2811
+ const asked = url.searchParams.get("kind") ?? "auto";
2812
+ const kind = kinds.includes(asked as EnrichKind) ? (asked as EnrichKind) : "auto";
2813
+ const year = Number(url.searchParams.get("year")) || null;
2814
+ const match = await options.enricher.lookup(name, kind, year);
2815
+ response.writeHead(200, {
2816
+ ...CORS,
2817
+ "content-type": "application/json; charset=utf-8",
2818
+ // A miss is worth asking again in a few hours; a hit lasts the day.
2819
+ "cache-control": match ? "public, max-age=3600" : "public, max-age=600",
2820
+ });
2821
+ response.end(JSON.stringify({ match }));
2822
+ return;
2823
+ }
2824
+
2792
2825
  if (path === "/api/links/play" && request.method === "POST") {
2793
2826
  let body: { url?: unknown } = {};
2794
2827
  try {
@@ -4069,6 +4102,13 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
4069
4102
  });
4070
4103
  // Channels as HLS, on demand, for Safari on a phone: one ffmpeg copying a
4071
4104
  // channel's fragments into short files while somebody is asking for them.
4105
+ // What things are, from nichedb.dev, remembered beside the keys so a
4106
+ // library is asked about once across restarts.
4107
+ const enricher = new Enricher({
4108
+ site: process.env["NIXAMP_NICHEDB"] || NICHEDB,
4109
+ cacheFile: join(stateDir(), "enrich.json"),
4110
+ onEvent: (message) => console.log(message),
4111
+ });
4072
4112
  const hls = new HlsPackagers({
4073
4113
  ffmpeg: tools.ffmpeg,
4074
4114
  listen: (id, listener) => channels.listen(id, listener),
@@ -4445,6 +4485,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
4445
4485
  ytdlp: tools.ytdlp ?? null,
4446
4486
  cookies: cookiesFile(),
4447
4487
  hls,
4488
+ enricher,
4448
4489
  ...(tls ? { tls } : {}),
4449
4490
  // Untagged, so a directory of five thousand files answers at once; the
4450
4491
  // tags follow through `tag` below.
@@ -4889,6 +4930,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
4889
4930
 
4890
4931
  const shutdown = (): void => {
4891
4932
  rtmp?.stop();
4933
+ enricher.save();
4892
4934
  hls.stopAll();
4893
4935
  channels.stopAll();
4894
4936
  ingest?.stopRtmp();
@@ -1 +1 @@
1
- import{t as e}from"./index-g9xRD0Ln.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-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};
@@ -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-CHT5Rca0.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-5rFzjEr5.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 ge=.14,_e=.02;function ve(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 ye(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 be(e,t,n=ge){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function xe(e,t,n=_e){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function Se(e,t,n,r,i){let{width:a,height:o}=t;if(a<=0||o<=0||n.length===0)return;e.clearRect(0,0,a,o);let s=a/n.length,c=Math.max(1,s*.72),l=Math.max(2,o*.012);e.fillStyle=i.bar,n.forEach((t,n)=>{let r=Math.max(1,t*(o-l*2));e.fillRect(n*s+(s-c)/2,o-r,c,r)}),e.fillStyle=i.peak,r.forEach((t,n)=>{let r=o-Math.max(1,t*(o-l*2))-l*2;e.fillRect(n*s+(s-c)/2,Math.max(0,r),c,l)})}var Ce=`nixamp.remote`,we=`nixamp.volume`,Te=`nixamp.listenHere`,Ee=!1;function T(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function De(){let n={status:T(`status`),source:T(`source`),install:T(`install`),video:T(`video`),audio:T(`audio`),title:T(`title-line`),album:T(`album-line`),meta:T(`meta-line`),metaBlurb:T(`meta-blurb`),liveLine:T(`live-line`),downloadNow:T(`download-now`),linkForm:T(`link-form`),linkUrl:T(`link-url`),goLiveNow:T(`go-live-now`),elapsed:T(`elapsed`),total:T(`total`),seek:T(`seek`),fullscreen:T(`fullscreen`),copyNow:T(`copy-now`),canvas:T(`spectrum`),glyphs:T(`glyphs`),levels:T(`levels`),playlist:T(`playlist`),crumbs:T(`crumbs`),filter:T(`filter`),playlistTitle:T(`playlist-panel`),note:T(`note`),files:T(`files`),folder:T(`folder`),remoteUrl:T(`remote-url`),remoteForm:T(`remote-form`),remoteState:T(`remote-state`),disconnect:T(`disconnect`),browse:T(`browse`),accountForm:T(`account-form`),accountEmail:T(`account-email`),accountPassword:T(`account-password`),accountSubmit:T(`account-submit`),accountToggle:T(`account-toggle`),accountProviders:T(`account-providers`),accountPanel:T(`account-panel`),accountElsewhere:T(`account-elsewhere`),accountSignOut:T(`account-signout`),accountNote:T(`account-note`),adminPanel:T(`admin-panel`),adminNote:T(`admin-note`),adminSaid:T(`admin-said`),adminConnections:T(`admin-connections`),publishPanel:T(`publish-panel`),publishNote:T(`publish-note`),publishList:T(`publish-list`),adminRestream:T(`admin-restream`),adminReplace:T(`admin-replace`),adminSource:T(`admin-source`),adminName:T(`admin-name`),adminAdd:T(`admin-add`),homeNote:T(`home-note`),loadHome:T(`load-home`),directory:T(`directory`),recentNote:T(`recent-note`),recentList:T(`recent-list`),followingNote:T(`following-note`),followingList:T(`following-list`),serversPanel:T(`servers-panel`),serversNote:T(`servers-note`),serversList:T(`servers-list`),favoritesPanel:T(`favorites-panel`),favoritesNote:T(`favorites-note`),favoritesList:T(`favorites-list`),favHere:T(`fav-here`),catalogsPanel:T(`catalogs-panel`),catalogsNote:T(`catalogs-note`),catalogsForm:T(`catalogs-form`),catalogSource:T(`catalog-source`),catalogName:T(`catalog-name`),catalogsList:T(`catalogs-list`),catalogsCrumbs:T(`catalogs-crumbs`),catalogsFilter:T(`catalogs-filter`),catalogsEntries:T(`catalogs-entries`),notifyPanel:T(`notify-panel`),notifyNote:T(`notify-note`),notifyWeb:T(`notify-web`),notifyEmail:T(`notify-email`),notifySms:T(`notify-sms`),notifyPhone:T(`notify-phone`),notifyPhoneForm:T(`notify-phone-form`),notifyPhoneNote:T(`notify-phone-note`),directoryNote:T(`directory-note`),directoryList:T(`directory-list`),onairPanel:T(`onair-panel`),onairNote:T(`onair-note`),onairList:T(`onair-list`),sharePanel:T(`share-panel`),shareNote:T(`share-note`),shareLink:T(`share-link`),shareCopy:T(`share-copy`),sharePhone:T(`share-phone`),shareSend:T(`share-send`),liveControls:T(`live-controls`),goLive:T(`go-live`),stopLive:T(`stop-live`),shareTo:T(`share-to`),listenOnly:T(`listen-only`),listenHere:T(`listen-here`),volume:T(`volume`),prev:T(`prev`),playPause:T(`play-pause`),stop:T(`stop`),next:T(`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,I();try{return await e()}finally{--re,I()}}let S=null,C=null,w=null,ge=``;function _e(e,t,n=null){let r=`${t}|${e}`;if(ge=r,w?.key===r||(w=null,o!==`remote`||e.trim()===``))return;let i=new URLSearchParams({name:e,kind:t});n&&i.set(`year`,String(n)),fetch(j.url(`/api/enrich?${i}`)).then(e=>e.ok?e.json():{match:null}).then(e=>{ge===r&&(w={key:r,match:e.match??null},I())}).catch(()=>void 0)}let De=!1,E=``,Oe=``,ke=null,Ae=``,D=Array(24).fill(0),O=Array(24).fill(0),je=[],k=()=>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[N()];return e&&(A.source!==``||k())?{kind:`track`,index:N(),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…`,I();try{let r=``;if(e.kind===`track`)await j.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(j.url(t),{method:`POST`}),a=await i.json().catch(()=>({}));if(!i.ok){_=a.error??`${n} would not go on the air.`,I();return}r=`channel:${e.kind===`entry`?a.channel??``:e.id}`}De?await fetch(j.url(`/api/live/start`),{method:`POST`}).catch(()=>void 0):await gn(!0),await Y(),X();let i=un(r),a=E?` Call ${Oe||`the line`} and key ${E} to talk about it.`:``;i===``?_=`${n} is on the air.${a}`:(await Q(i,t,`✓`),_=`${n} is on the air. Link copied.${a}`),I()}catch{_=`could not reach the server`,I()}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 A=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),I()},onEnded:()=>{sn()||o===`remote`&&y<0&&!k()||F(1)},onState:()=>I(),onBusy:e=>{ae!==e&&(ae=e,I())},onError:e=>{sn()||(_=e,I(),nn())}}),j=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&&(A.seek(t),setTimeout(()=>A.seek(t),600))})}}k()&&(D=e.bars.length>0?e.bars:D,O=xe(O,D)),I()},onStatus:(e,t)=>{h=e,g=t??``,I()}}),M=()=>o===`remote`?m.tracks.length:f.length,N=()=>o===`remote`?k()||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[N()]:f[N()];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[N()]:f[N()])?.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: `,$(t)],i=b?C?.channels.find(e=>e.id===b?.id)?.code??``:De?E:``;i&&r.push(`. To talk about it, call `,$(Oe||`the line`),` and key `,$(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=()=>k()?m.tracks[N()]?.duration??0:A.duration,Ve=()=>k()?m.position:A.position,He=()=>k()?m.playing:A.playing;async function P(e){if(o===`remote`){if(k()){await j.send({type:`play`,index:e});return}await Ue(e);return}let t=f[e];t&&(p=e,b=null,S={kind:`file`},await x(()=>A.load(t,!0)),it(t.video),at(),I())}async function Ue(e){let t=m.tracks[e];t&&(y=e,b=null,S={kind:`file`},_e(t.title,`auto`),await x(()=>A.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:j.media(e,0),video:t.video===!0,objectUrl:!1},!0)),it(t.video===!0),at())}async function We(){if(k()){await j.send({type:`toggle`});return}M()!==0&&(A.playing?A.pause():A.position>0?await A.play():await P(N()),I())}async function F(e){let t=M();if(t!==0){if(k()){await j.send({type:e>0?`next`:`prev`});return}await P((N()+e+t)%t)}}async function Ge(){if(k()){await j.send({type:`stop`});return}b=null,S=null,A.stop(),D=Array(24).fill(0),O=[...D],I()}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=b?C?.channels.find(e=>e.id===b?.id):void 0,a=A.source===``&&!b&&!(k()&&m.tracks[N()]);if(!a){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`&&k()?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`),i?(t.push(i.via===`pull`?`${i.listeners} watching`:`${i.listeners} listening · over ${i.via}`),i.startedAt>0&&t.push(`on air ${e(Math.max(0,(Date.now()-i.startedAt)/1e3))}`),i.redials&&t.push(`redialled ${i.redials}×`),Me()&&i.error&&t.push(i.error)):o===`remote`&&!b?(m.tracks[N()]&&M()>0&&t.push(`track ${N()+1} of ${M()}`),k()&&C&&t.push(`${C.server.playing?`playing`:`stopped`} on ${s||`the server`}`)):o===`local`&&M()>0&&t.push(`track ${N()+1} of ${M()}`),S?.catalog!==void 0&&(S.kind===`channel`?b!==null:S.kind===`vod`&&A.source!==``)&&S?.catalog&&(t.push(S.entry?.group?`${S.catalog.name} › ${S.entry.group}`:S.catalog.name),r=S.entry?.logo??``);let a=w?.key===ge?w.match:null;if(a){a.image&&(r=a.image);let e=a.data;if(a.kind===`title`){a.year&&t.push(String(a.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(a.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)}else if(a.kind===`fixture`){let n=typeof e.statusDetail==`string`?e.statusDetail:``;n&&t.push(n)}}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`)}De&&E&&k()&&!b&&S?.kind!==`live`&&t.push(Oe?`☎ ${Oe} · key ${E}`:`☎ code ${E}`)}let c=w?.key===ge?w.match:null,l=!a&&c?.summary?c.summary:``,u=`${r}|${t.join(`|`)}|${l}`;if(u===Je)return;Je=u,n.meta.hidden=t.length===0,n.metaBlurb.textContent=l,n.metaBlurb.hidden=l===``;let d=[];if(r!==``&&/^https?:\/\//.test(r)){let e=document.createElement(`img`);e.className=c?.kind===`title`&&r===c.image?`meta-logo meta-poster`:`meta-logo`,e.alt=``,e.src=r,e.addEventListener(`error`,()=>{e.hidden=!0}),d.push(e)}for(let e of t){let t=document.createElement(`span`);t.className=`meta-chip`,t.textContent=e,d.push(t)}n.meta.replaceChildren(...d)}function I(){let t=M(),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=A.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||k()),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||j.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=D.map(qe).join(``);let[p,ee]=k()?m.levels:A.levels();n.levels.textContent=Ke(p,ee)}let Xe=``,Ze=-1,L=``;function Qe(e){if(n.crumbs.hidden=!e,!e)return;let t=L===``?[]:L.split(`/`),r=(e,t,n)=>{if(n){let t=document.createElement(`span`);return t.className=`here`,t.textContent=e,t}let r=document.createElement(`button`);return r.type=`button`,r.textContent=e,r.addEventListener(`click`,()=>{L=t,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`,()=>{L=L===``?e:`${L}/${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!==``||L===``||e===L||e.startsWith(`${L}/`),l=e=>a!==``||e===L,u=e=>{let t=L===``?e:e.slice(L.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}:${L}:${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||L!==``));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(),Q(un(`track:${n.index}`,y===n.index?A.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=N(),_=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(j.url(`/api/source/remove`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({group:e})}),n=await t.json();z(t.ok?`Removed ${n.removed??0} tracks from ${e}.`:n.error??`that did not work`)}catch{z(`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(k())O=xe(O,D);else{let e=A.read();e.length>0&&(je.length!==25&&(je=ve(24,e.length)),D=be(D,ye(e,je)),O=xe(O,D))}if(o){let e=getComputedStyle(document.documentElement);Se(o,{width:t.width,height:t.height},D,O,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(He()){n.glyphs.textContent=D.map(qe).join(``);let[t,r]=k()?m.levels:A.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 F(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void F(-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)&&P(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 F(-1)),n.next.addEventListener(`click`,()=>void F(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.`,I();return}_=`Reading ${e}…`,await x(async()=>{let t,n={};try{t=await fetch(j.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 on({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}})}),I()}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(j.url(`/api/links/download?url=${encodeURIComponent(e.url)}${t}`),`_blank`),_=`Fetching it through the server; your browser will save it when it arrives.`,I()}),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&&A.seek(Number(n.seek.value)/1e3*e),v=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;A.volume=e;try{localStorage.setItem(we,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.`,I();return}te(f),f=t,p=0,o=`local`,j.close(),_=``,P(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.`,I();return}(async()=>{h=`connecting`,I();let e=he(r);if(e){h=`error`,g=e,_=e,o=`local`,I();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`,I();return}let n=await pe(r,i);if(n){h=`error`,g=n,_=n,o=`local`,I();return}o=`remote`,_=``;try{localStorage.setItem(Ce,t.trim())}catch{}j.connect(t),X(),kt(),tn(!0),gt(),Y(),I()})()});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.`:J?`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),J&&e.append(St(t.url,t.name)),t.ownerId&&J&&t.ownerId!==J&&e.append(Ht(t.ownerId,t.name)),d.childElementCount>0&&e.append(d),t.ownerId&&J&&t.ownerId===J){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 R=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 z(e){n.adminSaid.textContent=e,n.adminSaid.hidden=e===``}let mt=async()=>{try{let e=await fetch(j.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)),hn(t.home??``,t.root??``),X()}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,R&&clearInterval(R),R=null;return}let e=!1,t=null,r=!1;try{let n=await fetch(j.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,R&&clearInterval(R),R=null,X(),kt(),Y(),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(),R=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)}`;z(`Starting ${t||e}…`),(async()=>{try{let a=await fetch(j.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){z(o.error??`that did not work`);return}z(`${o.channel?.name||t||e} is on the air.`),n.adminSource.value=``,n.adminName.value=``,Y(),X()}catch{z(`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;z(`Reading ${e}…`);let t=n.adminReplace.checked,r=n.adminName.value.trim();(async()=>{try{let i=await fetch(j.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();z(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=``,Y(),X())}catch{z(`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=J?e.filter(e=>e.ownerId&&e.ownerId!==J):[];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)}},B=new Set,V=e=>{try{return new URL(e).origin}catch{return e}},H=e=>[...B].some(t=>V(t)===V(e));async function bt(){if(!J){B=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??[];B=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.`,I();return}}catch{_=`could not reach nixamp.com`,I();return}if(n)B.add(e);else for(let t of[...B])V(t)===V(e)&&B.delete(t);await bt()}function St(e,t){let n=document.createElement(`button`);n.type=`button`,n.className=`heart`;let r=()=>{let t=H(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=[...B].find(t=>V(t)===V(e))??e;xt(H(e)?i:e,t,!H(e)).then(r)}),n}function Ct(){let e=o===`remote`?j.shareLink:``;if(n.favHere.hidden=!(J&&e),n.favHere.hidden)return;let t=H(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`,()=>{Q(A.source,n.copyNow,`✓`)}),n.favHere.addEventListener(`click`,()=>{let e=dn()||j.shareLink;if(!e)return;let t=[...B].find(t=>V(t)===V(e))??e;xt(H(e)?t:e,s||j.address,!H(e)).then(Ct)});let U=[],W=null,G=null,wt=``,K=[],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(j.url(`/api/catalogs`))}catch{n.catalogsPanel.hidden=!0;return}if(!e.ok){n.catalogsPanel.hidden=!0;return}U=(await e.json().catch(()=>({}))).catalogs??[],W&&=U.find(e=>e.id===W?.id)??null,W||(G=null),n.catalogsPanel.hidden=!1,At()}function At(){let e=!n.adminPanel.hidden;n.catalogsForm.hidden=!e;let t=U.reduce((e,t)=>e+t.live,0),r=U.reduce((e,t)=>e+t.vod,0);n.catalogsNote.textContent=U.length===0?e?`No catalogs yet. Add an m3u list of channels or films.`:`No catalogs yet.`:`${U.length} ${U.length===1?`catalog`:`catalogs`} · ${t} live ${t===1?`channel`:`channels`} · ${r} on demand`,jt();let i=W!==null&&G!==null;if(n.catalogsList.hidden=i,n.catalogsFilter.hidden=!i,n.catalogsEntries.hidden=!i,i){It();return}if(W){Nt(W);return}n.catalogsList.replaceChildren(...U.map(t=>Mt(t,e)))}function jt(){let e=W!==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`,()=>{W=null,G=null,At()},!1),r(),t(W?.name??``,()=>{G=null,At()},G===null)];G!==null&&i.push(r(),t(G===``?`All groups`:G,()=>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`,()=>{W=e,G=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(j.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}.`,I();return}if(W?.id!==e.id||G!==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`,()=>{G=t,wt=``,n.catalogsFilter.value=``,K=[],Tt=0,At(),Ft(0)}),o.append(s,u),o}async function Ft(e){let t=W,n=G;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(j.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}.`,I();return}r===Dt&&(Tt=a.total??0,K=e===0?a.entries??[]:[...K,...a.entries??[]],It())}function It(){let t=W;if(!t)return;let r=K.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`;Q(j.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(K.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(K.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 (${K.length.toLocaleString()} of ${Tt.toLocaleString()})`,t.addEventListener(`click`,e=>{e.stopPropagation(),Ft(K.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(j.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 on({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},_e(a,`title`),await A.load({title:a,artist:``,album:``,duration:0,url:j.url(i.url),video:!0,objectUrl:!1},!0),it(!0),_=`Playing ${a}.`;return}_=`${t.title} would not play.`})}finally{n?.classList.remove(`loading`),I()}}async function Rt(e){z(`Reading ${e.name} again…`);try{let t=await fetch(j.url(`/api/catalogs/${encodeURIComponent(e.id)}/refresh`),{method:`POST`}),n=await t.json().catch(()=>({}));z(t.ok?`${n.catalog?.name??e.name}: ${(n.catalog?.entries??0).toLocaleString()} entries.`:n.error??`that did not work`)}catch{z(`could not reach the server`)}kt()}async function zt(e){try{z((await fetch(j.url(`/api/catalogs/${encodeURIComponent(e.id)}`),{method:`DELETE`})).ok?`${e.name} is off the server.`:`that did not work`)}catch{z(`could not reach the server`)}W?.id===e.id&&(W=null,G=null),kt()}n.catalogsForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.catalogSource.value.trim(),r=n.catalogName.value.trim();t&&(async()=>{z(`Reading ${r||t}…`);try{let e=await fetch(j.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){z(i.error??`that did not work`);return}z(`${i.catalog?.name??r??t}: ${(i.catalog?.entries??0).toLocaleString()} entries.`),n.catalogSource.value=``,n.catalogName.value=``}catch{z(`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 q=!1,J=``,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}.`:q?`Create an account on nixamp.com.`:`Listening needs no account. Sign in to keep favourites, follow people, and publish.`,n.accountSubmit.textContent=q?`Create account`:`Sign in`,n.accountToggle.textContent=q?`I have one`:`Create one`,n.accountPassword.autocomplete=q?`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();J=e.ok?t.account?.id??``:``,Yt(e.ok?t.account?.email??`you`:null)}catch{J=``,Yt(null)}bt(),Qt()};function Qt(){if(Ae===``)return;let e=Ae;Ae=``,n.remoteUrl.value=e,n.remoteForm.requestSubmit()}n.accountToggle.addEventListener(`click`,()=>{q=!q,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/${q?`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}J=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{}J=``,Yt(null),j.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,tn(!1);try{localStorage.removeItem(Ce)}catch{}_=`Signed out, and disconnected from the server.`,gt(),I()})()});try{let e=new URL(globalThis.location.href).searchParams,t=e.get(`url`)??``;t!==``&&(Ae=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`,()=>{j.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(),tn(!1),o=`local`,h=`idle`,g=``,I()});async function Y(){if(o!==`remote`||j.shareLink===``){n.sharePanel.hidden=!0;return}n.sharePanel.hidden=!1;let e=dn(),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(j.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(De=i.live,E=i.live?i.code:``,Oe=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 `),$(r),document.createTextNode(` and key `),$(i.code),document.createTextNode(`. That is a room with everyone else watching — not the stream itself.`)),n.shareSend.hidden=!1}}let $t=``,en=null,tn=e=>{en&&clearInterval(en),en=null,e&&(en=setInterval(()=>void X(),6e3))};async function X(){if(o!==`remote`){n.onairPanel.hidden=!0;return}let e;try{let t=await fetch(j.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(),I())}catch{n.onairPanel.hidden=!0;return}n.onairPanel.hidden=!1,C=e,fn(e);let t=`${n.adminPanel.hidden?`view`:`drive`}:${JSON.stringify(e)}`;if(t===$t)return;$t=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(pn({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){an(e.server.nowPlaying);return}l&&rn()},link:e.server.live?e.server.url:``,...c?{page:un(`live`)}:{},direct:c?j.url(`/api/live`):``}));for(let e of r)a.push(pn({title:e.name,detail:e.tracks===1?`re-streamed from the web`:`re-streamed from the web · ${e.tracks} tracks`,onPlay:()=>{P(e.at)},link:``,direct:j.media(e.at)}));for(let t of e.channels){let e=t.kind!==`audio`,n=j.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(pn({title:t.name,detail:r.join(` · `),onPlay:()=>{on({id:t.id,name:t.name,video:e})},link:n,page:un(`channel:${t.id}`),direct:n,onRestart:l&&t.via===`pull`?()=>{cn(t.id,t.name)}:void 0,onStop:l?()=>{ln(t.id,t.name)}:void 0}))}n.onairList.replaceChildren(...a)}async function nn(){if(o===`remote`)try{if((await fetch(j.media(N(),0),{method:`GET`,headers:{range:`bytes=0-1`}})).status!==402)return;_=J===``?`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.`,J===``&&n.accountPanel.scrollIntoView({behavior:`smooth`,block:`center`}),I()}catch{}}async function rn(){z(`Starting the stream on the server…`);try{await j.send({type:`play`,index:Math.max(0,N())})}catch{z(`could not reach the server`);return}await an(m.tracks[N()]?.title??``),z(`Playing to the room. Anybody with the view link sees this.`),await X()}async function an(e){y=-1,b=null,S={kind:`live`},_e(e,`auto`),await x(()=>A.load({title:e||`Live`,artist:``,album:``,duration:0,url:j.url(`/api/live`),video:!0,objectUrl:!1},!0)),it(!0),_=`Watching what this server is playing. Everyone here sees the same thing.`,I()}async function on(e,t=!0,n){y=-1,b=e,t&&(ne=0),t&&(S=n??{kind:`channel`}),t&&_e(e.name,S?.link?`auto`:`channel`);let r=e.video&&Ne();await x(()=>A.load({title:e.name,artist:``,album:``,duration:0,url:j.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.`),I()}function sn(){let e=b;return e?ke?!0:ne>=5?(_=`${e.name} stopped, and did not come back.`,b=null,S=null,I(),!0):(ne+=1,_=`${e.name} started over; rejoining…`,I(),ke=setTimeout(()=>{ke=null,b===e&&on(e,!1)},2e3),!0):!1}function Z(e){n.onairNote.textContent=e,z(e)}async function cn(e,t){Z(`Restarting ${t}…`);try{let n=await fetch(j.url(`/api/channels/${encodeURIComponent(e)}/restart`),{method:`POST`}),r=await n.json().catch(()=>({}));Z(n.ok?`${t} is dialling its source again.`:r.error??`that did not work`)}catch{Z(`could not reach the server`)}$t=``,X()}async function ln(e,t){Z(`Taking ${t} off the air…`);try{let n=await fetch(j.url(`/api/channels/${encodeURIComponent(e)}`),{method:`DELETE`}),r=await n.json().catch(()=>({}));Z(n.ok?`${t} is off the air.`:r.error??`that did not work`)}catch{Z(`could not reach the server`)}b?.id===e&&(b=null,A.stop()),$t=``,X()}async function Q(e,t,n=`Copied`){if(!e)return;let r=t.innerHTML;try{await navigator.clipboard.writeText(e)}catch{_=e,I();return}n===`✓`||n===`✓`?i(t,`check`):t.textContent=n,setTimeout(()=>{t.innerHTML=r},1200)}function un(e,t=0){let n=dn();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 dn(){if(d!==``)return d;let e=o===`remote`?j.shareLink:``;return/\/admin\//.test(e)?``:e}function fn(e){if(l===``)return;let t=l;if(t===`live`){l=``,e.server.playing?an(e.server.nowPlaying):(_=`Nothing is playing on this server right now.`,I());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=``,on({id:r.id,name:r.name,video:r.kind!==`audio`}))}function pn(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;Q(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=>{Q(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 mn=``;function hn(e,t){mn=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`,()=>{mn!==``&&(n.loadHome.disabled=!0,z(`Reading this server's files…`),(async()=>{try{let e=await fetch(j.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:mn})}),t=await e.json();z(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{z(`could not reach the server`)}finally{n.loadHome.disabled=!1}})())});let gn=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(j.url(e?`/api/live/start`:`/api/live/stop`),{method:`POST`}),r=await t.json();n.shareNote.textContent=t.ok?e?`Live. Anyone can call and key ${r.code??``} to talk about it.`:`Taken off the list. The link still works for anybody who has it.`:r.error??`that did not work`}catch{n.shareNote.textContent=`could not reach the server`}finally{n.goLive.disabled=!1,n.stopLive.disabled=!1,await Y()}};n.goLive.addEventListener(`click`,()=>void gn(!0)),n.stopLive.addEventListener(`click`,()=>void gn(!1));function $(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:dn()})}),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(Te,n.listenHere.checked?`1`:`0`)}catch{}o===`remote`&&(async()=>{n.listenHere.checked?(await j.send({type:`stop`}),await Ue(m.index)):(A.stop(),y=-1),I()})()}),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`:F(1);return;case`p`:case`ArrowLeft`:F(-1);return;case`ArrowDown`:e.preventDefault(),P(Math.min(M()-1,N()+1));return;case`ArrowUp`:e.preventDefault(),P(Math.max(0,N()-1));return}});let _n=null;globalThis.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),_n=e,n.install.hidden=!1}),n.install.addEventListener(`click`,()=>{_n?.prompt(),_n=null,n.install.hidden=!0});try{let e=localStorage.getItem(we);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),A.volume=Number(e));let t=localStorage.getItem(Ce);t&&(n.remoteUrl.value=t),localStorage.getItem(Te)===`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`,_=``,j.connect(e),I())})(),(()=>{if(Ee||Ae!==``)return;let e=()=>A.source!==``||A.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=()=>{Ee=!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})})})})(),I(),requestAnimationFrame(rt)}De(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
@@ -0,0 +1 @@
1
+ :root{--bg:#080c09;--panel:#0c120e;--edge:#1d2c22;--green:#4af689;--green-dim:#227a4a;--fg:#cfe8d8;--muted:#6d8a79;--warn:#e8c35a;--accent:#7ef0c4;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}[hidden]{display:none!important}html,body{background:var(--bg);min-height:100%;color:var(--fg);margin:0;font:14px/1.45 ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace}body{background-image:repeating-linear-gradient(#4af68906 0 1px,#0000 1px 3px)}#app{max-width:1100px;padding:calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));flex-direction:column;gap:10px;margin:0 auto;display:flex}.bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.brand{color:var(--green);letter-spacing:.14em;font-weight:700}.status{color:var(--muted)}.status[data-playing=true]{color:var(--green)}.status[data-playing=loading]{color:var(--accent)}.status[data-playing=loading]:before,.row.loading>.name:after{content:"";vertical-align:-.1em;border:2px solid;border-right-color:#0000;border-radius:50%;width:.8em;height:.8em;animation:.8s linear infinite turn;display:inline-block}.status[data-playing=loading]:before{margin-right:6px}.row.loading>.name:after{color:var(--accent);margin-left:8px}@keyframes turn{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.status[data-playing=loading]:before,.row.loading>.name:after{animation-duration:2.4s}}.chip{color:var(--muted);border:1px solid var(--edge);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:46vw;margin-left:auto;padding:1px 10px;font-size:12px;overflow:hidden}.panel{border:1px solid var(--edge);background:var(--panel);border-radius:6px;min-width:0;padding:14px 12px 12px;position:relative}.panel:before{content:attr(data-title);background:var(--panel);color:var(--green-dim);letter-spacing:.06em;padding:0 6px;font-size:12px;position:absolute;top:-.72em;left:10px}.split{grid-template-columns:1.3fr 1fr;gap:10px;display:grid}.split>.col-a{grid-column:1}.split>.col-b{grid-column:2}.stack{align-content:start;gap:14px;min-width:0;display:grid}.directory-list li.onair{border-bottom:1px solid var(--edge);flex-direction:column;align-items:stretch;gap:6px;padding:6px 0}.directory-list li.onair:last-child{border-bottom:0}.directory-list li.onair>.recent-label{flex:none}.onair-actions{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.icon{border:1px solid var(--edge);min-width:2.2em;height:2.2em;color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;font-size:1em;line-height:1}.icon:hover{color:var(--accent);border-color:var(--accent)}.directory-list li>.button:disabled{opacity:.4;cursor:not-allowed}.heart{color:var(--muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:3px;padding:2px 6px;font-size:1.1em;line-height:1}.heart:hover{color:var(--accent);border-color:var(--accent)}.heart[data-on=yes]{color:var(--accent)}.directory-list li>.server-label{flex-direction:column;flex:auto;gap:2px;min-width:0;padding:4px 2px;display:flex}.directory-list li>.server-label .name{font-weight:600}.directory-list li>.server-label .detail{color:var(--muted);white-space:normal}.directory-list li>.server-label .live{color:var(--green)}@media (max-width:720px){.split{grid-template-columns:1fr}.split>.col-a,.split>.col-b{grid-column:auto}}.track-title{color:var(--accent);text-overflow:ellipsis;white-space:nowrap;font-weight:700;overflow:hidden}.track-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.track-live{color:var(--muted);overflow-wrap:anywhere;margin-top:6px;line-height:1.5}.track-live b{color:var(--green);font-weight:700}.track-meta{flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px;margin-bottom:4px;display:flex}.meta-chip{color:var(--muted);border:1px solid var(--edge);letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:100%;padding:0 8px;font-size:11px;line-height:1.7;overflow:hidden}.meta-chip:first-of-type{color:var(--green);border-color:var(--green-dim)}.meta-logo{object-fit:contain;background:#000;border-radius:3px;max-width:64px;height:22px}.meta-logo.meta-poster{max-width:52px;height:72px}.track-blurb{color:var(--muted);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:4px 0 0;font-size:12px;line-height:1.5;display:-webkit-box;overflow:hidden}#video{border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px}.scrub{align-items:center;gap:10px;margin-top:6px;display:flex}.time{color:var(--fg);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}input[type=range]{appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:14px}input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(var(--edge), var(--edge));border:1px solid var(--edge);border-radius:3px;height:6px}input[type=range]::-moz-range-track{background:var(--edge);border-radius:3px;height:6px}input[type=range]::-webkit-slider-thumb{appearance:none;background:var(--green);border-radius:2px;width:10px;height:16px;margin-top:-6px}input[type=range]::-moz-range-thumb{background:var(--green);border:0;border-radius:2px;width:10px;height:16px}input[type=range]:disabled{opacity:.45;cursor:default}#spectrum{border:1px solid var(--edge);background:#060a07;border-radius:4px;width:100%;height:190px;display:block}.meters{align-items:baseline;gap:10px;margin-top:6px;display:flex;overflow:hidden}.glyphs{color:var(--green);letter-spacing:1px;white-space:pre;flex:auto;min-width:0;min-height:1.4em;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;overflow:hidden}.levelmeter{color:var(--accent);white-space:pre;font-variant-ligatures:none;text-align:right;flex:none;min-width:15ch;margin-left:auto;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace}.filter{border:1px solid var(--edge);width:100%;color:var(--fg);font:inherit;background:#0a100c;border-radius:4px;margin-bottom:6px;padding:4px 8px}.filter:focus{border-color:var(--accent);outline:none}.crumbs{color:var(--muted);flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:6px;font-size:12px;display:flex}.crumbs button{font:inherit;color:var(--accent);cursor:pointer;background:0 0;border:0;padding:0 2px}.crumbs button:hover{text-decoration:underline}.crumbs .here{color:var(--fg)}.catalog-tag{color:var(--muted);letter-spacing:.06em;flex:none;font-size:11px}.catalog-live{color:var(--green)}#catalogs-entries{max-height:320px;overflow-y:auto}.folder{cursor:pointer;white-space:nowrap;color:var(--accent);border-radius:3px;gap:8px;padding:2px 6px;display:flex}.folder:hover{background:#142019}.folder .name{text-overflow:ellipsis;flex:1;overflow:hidden}.folder .count{color:var(--muted);flex:none}.playlist{scrollbar-color:var(--green-dim) transparent;max-height:214px;margin:0;padding:0;list-style:none;overflow-y:auto}.row{cursor:pointer;white-space:nowrap;border-radius:3px;gap:8px;padding:2px 6px;display:flex}.row:hover{background:#142019}.row.selected{background:#16241c}.row.selected .name{color:var(--accent)}.row.playing .name{color:var(--green)}.group{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;border-top:1px solid var(--edge);align-items:center;gap:8px;margin-top:4px;padding:6px 6px 2px;font-size:11px;display:flex}.group:first-child{border-top:0;margin-top:0}.group-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.group-remove{border:1px solid var(--edge);color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;line-height:1.4}.group-remove:hover{color:var(--accent);border-color:var(--accent)}.row .n{color:var(--muted);text-align:right;flex:none;width:2.4em}.row .name{text-overflow:ellipsis;flex:1;overflow:hidden}.row .time{color:var(--muted);flex:none}.row-copy{color:var(--muted);cursor:pointer;opacity:.55;background:0 0;border:1px solid #0000;border-radius:3px;flex:none;padding:0 5px;line-height:1.4}.row:hover .row-copy,.row.selected .row-copy{opacity:1}.row-copy svg,.icon svg{vertical-align:-2px;width:14px;height:14px;display:inline-block}.icon{justify-content:center;align-items:center;display:inline-flex}.row-copy:hover{color:var(--accent);border-color:var(--accent)}.transport{border:1px solid var(--edge);background:var(--panel);border-radius:6px;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 10px;display:flex}button{font:inherit;color:var(--fg);border:1px solid var(--edge);cursor:pointer;background:#121a15;border-radius:4px;padding:6px 12px}button:hover{border-color:var(--green-dim);color:var(--green)}button:active{transform:translateY(1px)}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid var(--green);outline-offset:2px}button.primary{color:var(--green);border-color:var(--green-dim);min-width:64px}button.ghost{color:var(--muted);background:0 0}button.golive{color:#ff5c5c;letter-spacing:.06em;border-color:#7a2b2b}button.golive:hover{color:#fff;background:#7a2b2b;border-color:#ff5c5c}button.golive:disabled{opacity:.5;cursor:progress}.row-live:hover{color:#ff5c5c;border-color:#ff5c5c}.directory-list li:has(>.server-lives){flex-wrap:wrap}.directory-list li>.server-lives{border-left:2px solid var(--green-dim);flex-direction:column;flex:100%;gap:4px;margin:2px 0 0;padding:0 0 0 12px;list-style:none;display:flex}.server-lives li{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.server-lives li>.detail{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.server-lives li>.button{flex:none;padding:2px 10px;font-size:12px}.volume{flex:1;align-items:center;gap:8px;min-width:120px;max-width:220px;margin-left:auto;display:flex}.vol{color:var(--muted);letter-spacing:.08em;font-size:12px}.picker{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.link-form{margin-top:8px}.link-form input{flex:240px;min-width:0}.button{border:1px solid var(--edge);cursor:pointer;color:var(--fg);background:#121a15;border-radius:4px;padding:6px 12px;display:inline-block}.button:hover{border-color:var(--green-dim);color:var(--green)}.button input[type=file]{display:none}#remote-url{font:inherit;min-width:12ch;color:var(--fg);border:1px solid var(--edge);background:#060a07;border-radius:4px;flex:1;padding:6px 10px}.check{color:var(--muted);align-items:center;gap:8px;margin-top:10px;display:flex}.check input{accent-color:var(--green)}.hint{color:var(--muted);margin:0 0 8px;font-size:13px}.hint code{color:var(--accent)}#remote-state[data-status=live]{color:var(--green)}#remote-state[data-status=connecting],#remote-state[data-status=error]{color:var(--warn)}.note{color:var(--warn);border-left:2px solid var(--warn);margin:0;padding-left:8px}.statusbar{color:var(--muted);border-top:1px solid var(--edge);flex-wrap:wrap;gap:14px;padding-top:8px;font-size:12px;display:flex}.statusbar b{color:var(--green);font-weight:700}.statusbar .spacer{flex:1}.statusbar a{color:var(--muted)}.directory{border-top:1px solid var(--line);margin-top:.6rem;padding-top:.6rem}.directory-list{max-height:12rem;margin:0;padding:0;list-style:none;overflow-y:auto}.directory-list li+li{margin-top:.3rem}.directory-list button{border:1px solid var(--line);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:4px;padding:.4rem .5rem}.directory-list button:hover,.directory-list button:focus-visible{border-color:var(--accent);background:#ffffff0a}.directory-list .name{color:var(--accent);display:block}.directory-list .detail{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:.85em;display:block;overflow:hidden}.publish-list{margin:.3rem 0 0;padding:0;list-style:none}.publish-list li{align-items:center;gap:8px;margin-top:.3rem;display:flex}.publish-list li.in-use .slot{color:var(--green)}.publish-list .slot{color:var(--muted);flex:none;min-width:4.5em;font-size:12px}.publish-list input{flex:auto;min-width:0}.said{color:var(--accent);margin:.3rem 0 0}.directory-list li.offline .recent-label{opacity:.55}.directory-list li.offline .detail{color:var(--warn)}.share-line{align-items:center;gap:8px;margin:.4rem 0;display:flex}.share-what{color:var(--muted);letter-spacing:.06em;text-transform:uppercase;flex:none;font-size:12px}.share-line input{flex:auto;min-width:0}#share-phone{color:var(--fg);font-size:1.05em}#share-phone b{color:var(--accent)}.admin-table{border-collapse:collapse;width:100%;min-height:5.5em;max-height:14rem;margin:.4rem 0;font-size:.85em;display:block;overflow-y:auto}.admin-table th{text-align:left;opacity:.6;padding:.2rem .4rem .2rem 0;font-weight:400}.admin-table td{white-space:nowrap;text-overflow:ellipsis;max-width:12rem;padding:.2rem .4rem .2rem 0;overflow:hidden}.admin-table td.network-public{color:var(--warning,#e0b341)}.admin-table td.network-private{color:var(--success,#7fd18b)}.admin-table tr.ended{opacity:.45}body.route-directory .player-only{display:none}.directory-list li{align-items:stretch;gap:.4rem;display:flex}.directory-list li>:first-child{flex:auto;min-width:0}.directory-list li>button:first-child{text-align:left;width:100%}.directory-list li>.button,.directory-list li>.ghost{white-space:nowrap;flex:none;align-items:center;display:flex}.directory-list .follow{white-space:nowrap;width:auto;color:var(--muted);flex:none;padding-inline:.6rem}.directory-list .follow[data-following=yes]{border-color:var(--accent);color:var(--accent)}.toggle{color:var(--muted);cursor:pointer;align-items:center;gap:.35rem;display:inline-flex}.toggle input{accent-color:var(--accent)}.recent-label{border:1px solid var(--line);border-radius:4px;flex:auto;min-width:0;padding:.4rem .5rem}
@@ -1 +1 @@
1
- import{t as e}from"./index-g9xRD0Ln.js";var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule||!o.call(e,`default`)?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=[[/^(hvc1|hev1)/i,`H.265`],[/^av01/i,`AV1`],[/^(vp09|vp9)/i,`VP9`],[/^(ec-3|ec3)/i,`Dolby Digital Plus audio`],[/^(ac-3|ac3)/i,`Dolby Digital audio`],[/^dts/i,`DTS audio`],[/^(mp4a\.69|mp4a\.6b)/i,`MP2 audio`],[/^mp3$/i,`MP3 audio`],[/^mp4a\.40\./i,`AAC audio`]];function d(e){if(!e)return null;for(let[t,n]of u)if(t.test(e))return n;return e}function f(e,t){if(!t||!e)return[];if(e===`audio`){if(/^mp4a\.40\./i.test(t))return[`audio/mp4; codecs="mp4a.40.2"`];if(/^mp3$/i.test(t))return[`audio/mpeg`,`audio/mp4; codecs="mp3"`];if(/^opus$/i.test(t))return[`audio/mp4; codecs="opus"`,`audio/mp4; codecs="Opus"`]}return[`${e}/mp4; codecs="${t}"`]}function p(e,t,n=``,r=`stream`){if(!e||typeof t!=`function`)return null;let i=[[`video`,e.videoCodec],[`audio`,e.audioCodec]],a=[];for(let[e,n]of i)if(n&&!f(e,n).some(e=>{try{return t(e)}catch{return!1}})){let e=d(n);e&&a.push(e)}if(a.length===0)return null;let o=a.length===1?a[0]:`${String(a[0])} and ${String(a[1])}`;return`This ${r} is ${String(o)}, which this browser cannot decode.${n?` ${n}`:``}`}var m=2e3,h=5e3,g=3;function _(e,t={}){return{enableWorker:t.enableWorker??!1,enableStashBuffer:!0,stashInitialSize:393216,liveBufferLatencyChasing:!1,liveBufferLatencyMaxLatency:5,liveBufferLatencyMinRemain:1,autoCleanupSourceBuffer:!0,autoCleanupMaxBackwardDuration:30,autoCleanupMinBackwardDuration:10,lazyLoad:!1,lazyLoadMaxDuration:60,lazyLoadRecoverDuration:30,seekType:`range`}}async function v(t,n={}){let{media:r,src:i,isTv:a}=t,o=await e(()=>import(`./mpegts-D-7xRRW6.js`).then(e=>l(e.default,1)),[]),s=o.default??o;if(!s.getFeatureList().mseLivePlayback)return t.onError(`This browser cannot play transport streams.`),{destroy:()=>void 0,levels:()=>[]};let c=_(a,n),u=null,d=!1,f=0,v=null,y=null,b=-1,x=0,S=()=>{v&&clearTimeout(v),y&&clearInterval(y),v=null,y=null},C=()=>{if(!u)return;let e=u;u=null;try{e.destroy()}catch{}},w=e=>{d||(d=!0,S(),C(),t.onError(e))},T=e=>{if(!d){if(f>=5){w(e);return}f+=1,S(),C(),t.onNotice(`Reconnecting\u2026 (${String(f)}/5)`),v=setTimeout(()=>{v=null,d||D()},m*2**(f-1))}},E=()=>{y&&clearInterval(y),b=r.currentTime,x=0,y=setInterval(()=>{if(!d&&u){if(r.paused||r.ended||r.seeking){x=0,b=r.currentTime;return}if(r.currentTime===b){x+=1,x>=g&&(x=0,T(`The stream stopped sending.`));return}b=r.currentTime,x=0}},h)};function D(){u=s.createPlayer({type:`mpegts`,isLive:t.live,url:i,withCredentials:n.withCredentials??!1},c),u.on(s.Events.MEDIA_INFO??`media_info`,(...e)=>{let t=e[0],r=p(t,e=>globalThis.MediaSource?.isTypeSupported?.(e)??!1,n.unplayableAdvice??``);r&&w(r)}),u.on(s.Events.ERROR??`error`,(...e)=>{let t=String(e[1]??``);T(`This stream could not be played (${t}).`)}),u.attachMediaElement(r),u.load(),E(),t.onReady?.({live:t.live,levels:[]})}let O=()=>{f=0,t.onNotice(null)};return r.addEventListener(`playing`,O),D(),{destroy(){d=!0,S(),r.removeEventListener(`playing`,O),C()},levels:()=>[]}}export{v as createMpegtsEngine,s as t};
1
+ import{t as e}from"./index-2_Frv88t.js";var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule||!o.call(e,`default`)?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=[[/^(hvc1|hev1)/i,`H.265`],[/^av01/i,`AV1`],[/^(vp09|vp9)/i,`VP9`],[/^(ec-3|ec3)/i,`Dolby Digital Plus audio`],[/^(ac-3|ac3)/i,`Dolby Digital audio`],[/^dts/i,`DTS audio`],[/^(mp4a\.69|mp4a\.6b)/i,`MP2 audio`],[/^mp3$/i,`MP3 audio`],[/^mp4a\.40\./i,`AAC audio`]];function d(e){if(!e)return null;for(let[t,n]of u)if(t.test(e))return n;return e}function f(e,t){if(!t||!e)return[];if(e===`audio`){if(/^mp4a\.40\./i.test(t))return[`audio/mp4; codecs="mp4a.40.2"`];if(/^mp3$/i.test(t))return[`audio/mpeg`,`audio/mp4; codecs="mp3"`];if(/^opus$/i.test(t))return[`audio/mp4; codecs="opus"`,`audio/mp4; codecs="Opus"`]}return[`${e}/mp4; codecs="${t}"`]}function p(e,t,n=``,r=`stream`){if(!e||typeof t!=`function`)return null;let i=[[`video`,e.videoCodec],[`audio`,e.audioCodec]],a=[];for(let[e,n]of i)if(n&&!f(e,n).some(e=>{try{return t(e)}catch{return!1}})){let e=d(n);e&&a.push(e)}if(a.length===0)return null;let o=a.length===1?a[0]:`${String(a[0])} and ${String(a[1])}`;return`This ${r} is ${String(o)}, which this browser cannot decode.${n?` ${n}`:``}`}var m=2e3,h=5e3,g=3;function _(e,t={}){return{enableWorker:t.enableWorker??!1,enableStashBuffer:!0,stashInitialSize:393216,liveBufferLatencyChasing:!1,liveBufferLatencyMaxLatency:5,liveBufferLatencyMinRemain:1,autoCleanupSourceBuffer:!0,autoCleanupMaxBackwardDuration:30,autoCleanupMinBackwardDuration:10,lazyLoad:!1,lazyLoadMaxDuration:60,lazyLoadRecoverDuration:30,seekType:`range`}}async function v(t,n={}){let{media:r,src:i,isTv:a}=t,o=await e(()=>import(`./mpegts-sgBnfefF.js`).then(e=>l(e.default,1)),[]),s=o.default??o;if(!s.getFeatureList().mseLivePlayback)return t.onError(`This browser cannot play transport streams.`),{destroy:()=>void 0,levels:()=>[]};let c=_(a,n),u=null,d=!1,f=0,v=null,y=null,b=-1,x=0,S=()=>{v&&clearTimeout(v),y&&clearInterval(y),v=null,y=null},C=()=>{if(!u)return;let e=u;u=null;try{e.destroy()}catch{}},w=e=>{d||(d=!0,S(),C(),t.onError(e))},T=e=>{if(!d){if(f>=5){w(e);return}f+=1,S(),C(),t.onNotice(`Reconnecting\u2026 (${String(f)}/5)`),v=setTimeout(()=>{v=null,d||D()},m*2**(f-1))}},E=()=>{y&&clearInterval(y),b=r.currentTime,x=0,y=setInterval(()=>{if(!d&&u){if(r.paused||r.ended||r.seeking){x=0,b=r.currentTime;return}if(r.currentTime===b){x+=1,x>=g&&(x=0,T(`The stream stopped sending.`));return}b=r.currentTime,x=0}},h)};function D(){u=s.createPlayer({type:`mpegts`,isLive:t.live,url:i,withCredentials:n.withCredentials??!1},c),u.on(s.Events.MEDIA_INFO??`media_info`,(...e)=>{let t=e[0],r=p(t,e=>globalThis.MediaSource?.isTypeSupported?.(e)??!1,n.unplayableAdvice??``);r&&w(r)}),u.on(s.Events.ERROR??`error`,(...e)=>{let t=String(e[1]??``);T(`This stream could not be played (${t}).`)}),u.attachMediaElement(r),u.load(),E(),t.onReady?.({live:t.live,levels:[]})}let O=()=>{f=0,t.onNotice(null)};return r.addEventListener(`playing`,O),D(),{destroy(){d=!0,S(),r.removeEventListener(`playing`,O),C()},levels:()=>[]}}export{v as createMpegtsEngine,s as t};