nixamp 0.6.0 → 0.6.4

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/invite.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Asking somebody to watch, when that somebody is not technical.
3
+ *
4
+ * A share link is a URL with a key in it, which is fine for the person who
5
+ * runs the server and useless as a thing to text your mother. An invite is the
6
+ * three ways in, written as a sentence: a link that opens a player, a phone
7
+ * number, and the code to key once it answers.
8
+ *
9
+ * The sender is signed in, because sending is an action with a cost: a text
10
+ * message is money and somebody's phone. The recipient signs in too, but only
11
+ * once and only at the far end of a single click, because a stream can ask to
12
+ * be paid for -- x402 starts charging past five listeners -- and there is
13
+ * nobody to charge without an account. The dial-in path is the exception and
14
+ * stays open to anybody, since a phone call cannot sign in to anything.
15
+ */
16
+
17
+ /** Where the phone line answers, and what to key when it does. */
18
+ export interface Invite {
19
+ /** What the stream is called, as the recipient will see it. */
20
+ name: string;
21
+ /** A link that opens a player on this stream, listen only. */
22
+ link: string;
23
+ /** The phone number, when this stream is one the line knows about. */
24
+ phone: string;
25
+ /** The six digits that reach this stream, when it has been published. */
26
+ code: string;
27
+ }
28
+
29
+ /** Looks like a phone number rather than an address. */
30
+ export function isPhone(value: string): boolean {
31
+ return /^\+?[\d\s().-]{7,20}$/.test(value.trim()) && /\d{7}/.test(value.replace(/\D/g, ""));
32
+ }
33
+
34
+ /** Looks like somewhere an email could arrive. */
35
+ export function isEmail(value: string): boolean {
36
+ return /^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(value.trim());
37
+ }
38
+
39
+ /**
40
+ * The message itself.
41
+ *
42
+ * Short, because it is going into a text message, and ordered by how likely
43
+ * each way in is to work for the person reading it. The link first: most
44
+ * people have a browser in their hand. The phone last, because it is the one
45
+ * that needs no browser at all and is therefore the fallback that never fails.
46
+ */
47
+ export function inviteText(invite: Invite): string {
48
+ const lines = [`${invite.name} is streaming.`, "", `Watch: ${invite.link}`];
49
+ if (invite.phone && invite.code) {
50
+ lines.push("", `Or call ${invite.phone} and key ${invite.code} to listen.`);
51
+ }
52
+ return lines.join("\n");
53
+ }
54
+
55
+ /** The same thing as a subject line, for the surface that wants one. */
56
+ export function inviteSubject(invite: Invite): string {
57
+ return `${invite.name} is streaming`;
58
+ }
59
+
60
+ /**
61
+ * A link that opens a player on this stream.
62
+ *
63
+ * Sent through nixamp.com when the stream is https, because that page is a
64
+ * player anybody can already open and reaches this stream with `?url=`. An
65
+ * http stream is sent as its own address instead: a browser refuses every
66
+ * request from an https page to an http one, so routing it through nixamp.com
67
+ * would produce a link that cannot work, which is worse than a plainer one
68
+ * that does.
69
+ */
70
+ export function watchLink(streamUrl: string, site: string): string {
71
+ const bare = streamUrl.replace(/\/+$/, "");
72
+ if (!bare.startsWith("https://")) return bare;
73
+ return `${site.replace(/\/+$/, "")}/?url=${encodeURIComponent(bare)}`;
74
+ }
package/src/playlist.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** The playlist: audio found on disk or named by a playlist, in a stable order. */
2
2
  import { readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { probe, type Tools, type Track } from "./audio.ts";
4
+ import { probe, probeAsync, type Tools, type Track } from "./audio.ts";
5
5
  import {
6
6
  type Entry,
7
7
  isHls,
@@ -146,15 +146,23 @@ export function loadPlaylist(tools: Tools, root: string, probeTags = true): Trac
146
146
  * ffprobe rather than for the whole library, and the tagging still finishes in
147
147
  * about the time it did.
148
148
  */
149
- export async function loadTagged(tools: Tools, source: string): Promise<Track[]> {
149
+ export async function loadTagged(
150
+ tools: Tools,
151
+ source: string,
152
+ /** Injected by the test, which must not depend on ffprobe being installed. */
153
+ probeOne: (tools: Tools, path: string) => Promise<Track> = probeAsync,
154
+ ): Promise<Track[]> {
150
155
  // A URL is one thing and is never probed; a playlist carries its own titles.
151
156
  if (isRemote(source) || isPlaylistFile(source)) return loadSource(tools, source, true);
152
157
 
153
158
  const paths = findAudio(source);
154
159
  const tracks: Track[] = [];
155
160
  for (const path of paths) {
156
- tracks.push(probe(tools, path));
157
- await new Promise<void>((done) => setImmediate(done));
161
+ // Awaiting a child process, not blocking on one. Yielding between files
162
+ // was not enough: each spawnSync still stopped everything for as long as
163
+ // one ffprobe took, which on a large file is long enough to strangle a
164
+ // stream being served at the same time.
165
+ tracks.push(await probeOne(tools, path));
158
166
  }
159
167
  return tracks;
160
168
  }
package/src/server.ts CHANGED
@@ -751,6 +751,12 @@ export interface HandlerOptions {
751
751
  * imported so the handler stays a plain function of a request.
752
752
  */
753
753
  load: (source: string) => Promise<Track[]>;
754
+ /**
755
+ * The same source, with its tags, read without holding the event loop. Called
756
+ * after `load` and never awaited: the titles arrive into a player that is
757
+ * already playing.
758
+ */
759
+ tag?: (source: string) => Promise<Track[]>;
754
760
  /**
755
761
  * The public directory, on the instance that hosts one. Only nixamp.com
756
762
  * passes this; a nixamp on your laptop is a publisher, not a registry.
@@ -1916,6 +1922,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1916
1922
  return;
1917
1923
  }
1918
1924
  engine.replace(tracks, source);
1925
+ // Names now, tags later, here as much as at startup: re-streaming a
1926
+ // directory of five thousand files used to read every tag before it
1927
+ // answered, with the event loop held the whole time.
1928
+ if (options.tag) {
1929
+ void options
1930
+ .tag(source)
1931
+ .then((tagged) => engine.retag(tagged, source))
1932
+ .catch(() => {});
1933
+ }
1919
1934
  json(response, 200, engine.snapshot());
1920
1935
  } catch (error) {
1921
1936
  json(response, 422, { error: (error as Error).message.replace(/^nixamp: /, "") });
@@ -1939,14 +1954,20 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1939
1954
  // to it raw is bytes it cannot play. Seeking is what this route is for
1940
1955
  // and transcoding gives it up, but an unseekable film beats a silent
1941
1956
  // one -- and the seekable formats are untouched.
1942
- if (playsInBrowser(file)) {
1957
+ // A ceiling the caller asked for, because only the caller knows what its
1958
+ // link can carry. Capped at both ends: nothing below 200k is watchable,
1959
+ // and above 20 megabits the original was always the better answer.
1960
+ const asked = Number(url.searchParams.get("kbps") ?? "");
1961
+ const capKbps = Number.isFinite(asked) && asked > 0 ? Math.min(20_000, Math.max(200, asked)) : 0;
1962
+
1963
+ if (playsInBrowser(file) && capKbps === 0) {
1943
1964
  sendFile(request, response, file);
1944
1965
  } else if (hasPicture(file)) {
1945
1966
  // A film. It used to arrive as MP3 with `-vn`, which is to say as a
1946
1967
  // soundtrack over a blank panel; what ffprobe finds inside decides how
1947
1968
  // little work it takes to keep the picture.
1948
1969
  const codecs = await codecsOf({ ffmpeg: [], ffprobe: options.ffprobe ?? ["ffprobe"], play: null }, file);
1949
- pipeFfmpeg(request, response, file, options.ffmpeg ?? ["ffmpeg"], videoArgs(codecs), "video/mp4");
1970
+ pipeFfmpeg(request, response, file, options.ffmpeg ?? ["ffmpeg"], videoArgs(codecs, capKbps), "video/mp4");
1950
1971
  } else {
1951
1972
  transcode(request, response, file, options.ffmpeg ?? ["ffmpeg"]);
1952
1973
  }
@@ -2577,7 +2598,10 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2577
2598
  ffmpeg: tools.ffmpeg,
2578
2599
  ffprobe: tools.ffprobe,
2579
2600
  ...(tls ? { tls } : {}),
2580
- load: (next) => loadSource(tools, next),
2601
+ // Untagged, so a directory of five thousand files answers at once; the
2602
+ // tags follow through `tag` below.
2603
+ load: (next) => loadSource(tools, next, false),
2604
+ tag: (next) => loadTagged(tools, next),
2581
2605
  ...(directory ? { directory } : {}),
2582
2606
  ...(follows ? { follows, vapidPublicKey } : {}),
2583
2607
  ...(partyLine ? { partyLine } : {}),
@@ -1 +1 @@
1
- import{t as e}from"./index-PgqcCBW4.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-0Asn6zxx.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-6P5P66VW.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-CEVDX7YC.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 x(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var S=2048;function C(e){return e===`audio`}var te=class{elements;handlers;attached=null;context=null;analyser=null;wired=new WeakSet;active;frequencies=new Uint8Array;constructor(e,t){this.elements=e,this.handlers=t,this.active=e.audio;for(let t of[e.audio,e.video])t.crossOrigin=`anonymous`,t.addEventListener(`timeupdate`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`loadedmetadata`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`ended`,()=>{t===this.active&&this.handlers.onEnded()}),t.addEventListener(`play`,()=>{t===this.active&&this.handlers.onState(!0)}),t.addEventListener(`pause`,()=>{t===this.active&&this.handlers.onState(!1)}),t.addEventListener(`error`,()=>{t===this.active&&this.handlers.onError(w(t))})}get playing(){return!this.active.paused&&!this.active.ended}get position(){return this.active.currentTime}get duration(){return Number.isFinite(this.active.duration)?this.active.duration:0}get showingVideo(){return this.active===this.elements.video}ensureGraph(e){let t=globalThis.AudioContext??globalThis.webkitAudioContext;if(t){if(this.context??=new t,this.analyser||(this.analyser=this.context.createAnalyser(),this.analyser.fftSize=S,this.analyser.smoothingTimeConstant=.6,this.analyser.connect(this.context.destination),this.frequencies=new Uint8Array(this.analyser.frequencyBinCount)),!this.wired.has(e))try{this.context.createMediaElementSource(e).connect(this.analyser),this.wired.add(e)}catch{this.wired.add(e)}this.context.resume()}}read(){return this.analyser&&this.analyser.getByteFrequencyData(this.frequencies),this.frequencies}levels(){if(!this.analyser)return[0,0];let e=this.read(),t=0;for(let n of e)t+=n;let n=e.length===0?0:t/e.length/255;return[Math.min(1,n*2.2),Math.min(1,n*2.2)]}async load(e,t){let n=e.objectUrl?e.video?`mp4`:`audio`:d({src:e.url}),r=e.video||!C(n)?this.elements.video:this.elements.audio;r!==this.active&&(this.active.pause(),this.active.removeAttribute(`src`),this.active.load(),this.active=r),this.attached?.destroy(),this.attached=null;try{this.attached=await h(this.active,{src:e.url,kind:n,unplayableAdvice:`VLC or mpv will play it; nixamp can only hand it to your browser.`,onError:e=>this.handlers.onError(e),onNotice:e=>{e&&this.handlers.onError(e)}})}catch(e){this.handlers.onError(e instanceof Error?e.message:`that would not play`);return}t&&await this.play()}async play(){this.ensureGraph(this.active);try{await this.active.play()}catch(e){this.handlers.onError(e instanceof Error?e.message:`playback was refused`)}}pause(){this.active.pause()}stop(){this.active.pause(),this.active.currentTime=0,this.attached?.destroy(),this.attached=null}seek(e){Number.isFinite(e)&&(this.active.currentTime=Math.max(0,e))}set volume(e){this.elements.audio.volume=e,this.elements.video.volume=e}get volume(){return this.active.volume}};function w(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 T(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function ne(e,t){return{...t,tracks:t.tracks??e.tracks}}function E(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 D(e,t){return`${e===``?``:E(e)}${t.startsWith(`/`)?t:`/${t}`}`}function O(e,t,n=0){return D(e,n>0?`/api/media/${t}?kbps=${Math.round(n)}`:`/api/media/${t}`)}function k(e){if(typeof e!=`object`||!e)return null;let t=e,n=T(),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}:{}}}):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 re=class{handlers;source=null;base=``;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}get connected(){return this.source!==null}connect(e){let t=E(e);this.close(),this.base=t,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let n=new EventSource(D(t,`/api/events`));this.source=n,n.onopen=()=>this.handlers.onStatus(`live`),n.onmessage=e=>{let t=k(A(e.data));t&&(t.revision<this.lastRevision||(this.lastRevision=t.revision,this.handlers.onStatus(`live`),this.handlers.onSnapshot(t)))},n.onerror=()=>{this.handlers.onStatus(`error`,`reconnecting…`)}}async send(e){if(this.base===``&&!this.connected)return;let t=await fetch(D(this.base,`/api/command`),{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=k(await t.json());n&&this.handlers.onSnapshot(n)}media(e,t=0){return O(this.base,e,t)}close(){this.source?.close(),this.source=null}};function A(e){try{return JSON.parse(e)}catch{return null}}async function ie(e,t){try{let n=await fetch(D(e,`/api/state`),{signal:t});return n.ok?k(await n.json()):null}catch{return null}}async function j(e,t){try{let n=await fetch(D(e,`/api/health`),{signal:t});if(!n.ok)return null;let r=await n.json();return r.name===`nixamp`?r.version??`unknown`:null}catch{return null}}function ae(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 M=.14,N=.02;function oe(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 se(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 ce(e,t,n=M){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function P(e,t,n=N){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function le(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 F=`nixamp.remote`,I=`nixamp.volume`;function L(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function R(){let n={status:L(`status`),source:L(`source`),install:L(`install`),video:L(`video`),audio:L(`audio`),title:L(`title-line`),album:L(`album-line`),elapsed:L(`elapsed`),total:L(`total`),seek:L(`seek`),canvas:L(`spectrum`),glyphs:L(`glyphs`),levels:L(`levels`),playlist:L(`playlist`),playlistTitle:L(`playlist-panel`),note:L(`note`),files:L(`files`),folder:L(`folder`),remoteUrl:L(`remote-url`),remoteForm:L(`remote-form`),remoteState:L(`remote-state`),disconnect:L(`disconnect`),browse:L(`browse`),accountForm:L(`account-form`),accountEmail:L(`account-email`),accountPassword:L(`account-password`),accountSubmit:L(`account-submit`),accountToggle:L(`account-toggle`),accountProviders:L(`account-providers`),accountPanel:L(`account-panel`),accountElsewhere:L(`account-elsewhere`),accountSignOut:L(`account-signout`),accountNote:L(`account-note`),adminPanel:L(`admin-panel`),adminNote:L(`admin-note`),adminConnections:L(`admin-connections`),adminRestream:L(`admin-restream`),adminSource:L(`admin-source`),directory:L(`directory`),recentNote:L(`recent-note`),recentList:L(`recent-list`),followingNote:L(`following-note`),followingList:L(`following-list`),serversPanel:L(`servers-panel`),serversNote:L(`servers-note`),serversList:L(`servers-list`),notifyPanel:L(`notify-panel`),notifyNote:L(`notify-note`),notifyWeb:L(`notify-web`),notifyEmail:L(`notify-email`),notifySms:L(`notify-sms`),notifyPhone:L(`notify-phone`),notifyPhoneForm:L(`notify-phone-form`),notifyPhoneNote:L(`notify-phone-note`),directoryNote:L(`directory-note`),directoryList:L(`directory-list`),listenHere:L(`listen-here`),volume:L(`volume`),prev:L(`prev`),playPause:L(`play-pause`),stop:L(`stop`),next:L(`next`)},r=`local`,i=[],a=0,o=T(),s=`idle`,c=``,l=`Pick files, or connect to a nixamp running somewhere else.`,u=!1,d=Array(24).fill(0),f=Array(24).fill(0),p=[],m=()=>r===`remote`&&!n.listenHere.checked,h=new te({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),R()},onEnded:()=>A(1),onState:()=>R(),onError:e=>{l=e,R()}}),g=new re({onSnapshot:e=>{o=ne(o,e),m()&&(d=e.bars.length>0?e.bars:d,f=P(f,d)),R()},onStatus:(e,t)=>{s=e,c=t??``,R()}}),_=()=>r===`remote`?o.tracks.length:i.length,v=()=>r===`remote`?o.index:a,y=()=>{if(r===`remote`){let e=o.tracks[o.index];return e?t(e):`Nothing loaded.`}let e=i[a];return e?t(e):`Nothing loaded.`},b=()=>(r===`remote`?o.tracks[o.index]:i[a])?.album||`—`,S=()=>m()?o.tracks[o.index]?.duration??0:h.duration,C=()=>m()?o.position:h.position,w=()=>m()?o.playing:h.playing;async function D(e){if(r===`remote`){if(m()){await g.send({type:`play`,index:e});return}await O(e);return}let t=i[e];t&&(a=e,await h.load(t,!0),V(t.video),H(),R())}async function O(e){let t=o.tracks[e];t&&(await h.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:g.media(e,0),video:t.video===!0,objectUrl:!1},!0),V(t.video===!0),H())}async function k(){if(m()){await g.send({type:`toggle`});return}_()!==0&&(h.playing?h.pause():h.position>0?await h.play():await D(v()),R())}async function A(e){let t=_();if(t!==0){if(m()){await g.send({type:e>0?`next`:`prev`});return}await D((v()+e+t)%t)}}async function M(){if(m()){await g.send({type:`stop`});return}h.stop(),d=Array(24).fill(0),f=[...d],R()}let N=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function R(){let t=_(),a=w();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=y(),n.album.textContent=b();let f=C(),p=S();n.elapsed.textContent=e(f),n.total.textContent=p>0?e(p):`--:--`,u||(n.seek.value=String(p>0?Math.round(f/p*1e3):0),n.seek.disabled=p<=0||m()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${g.address.replace(/^https?:\/\//,``)||`—`}`:i.length>0?`local · ${i.length} files`:`no source`,n.remoteState.textContent=r===`remote`?`${s}${c?` — ${c}`:``}`:`not connected`,n.remoteState.dataset.status=r===`remote`?s:`idle`,n.disconnect.hidden=r!==`remote`;let v=r===`remote`&&o.note!==``?o.note:l;n.note.textContent=v,n.note.hidden=v===``,ue(),n.glyphs.textContent=d.map(N).join(``);let[ee,x]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(x*6)).padEnd(6,`·`)}`}let z=``;function ue(){let a=r===`remote`?o.tracks.map(e=>[t(e),e.duration]):i.map(e=>[t(e),e.duration]),s=`${r}:${a.map(([e,t])=>`${e}@${t}`).join(`|`)}`;s!==z&&(z=s,n.playlist.replaceChildren(...a.map(([t,n],r)=>{let i=document.createElement(`li`);i.className=`row`,i.dataset.index=String(r);let a=document.createElement(`span`);a.className=`n`,a.textContent=String(r+1).padStart(2,` `);let o=document.createElement(`span`);o.className=`name`,o.textContent=t;let s=document.createElement(`span`);return s.className=`time`,s.textContent=n>0?e(n):`--:--`,i.append(a,o,s),i})));let c=v(),l=w();Array.from(n.playlist.children).forEach((e,t)=>{let n=e;n.classList.toggle(`selected`,t===c),n.classList.toggle(`playing`,t===c&&l)}),n.playlist.children[c]?.scrollIntoView({block:`nearest`})}function B(){let t=n.canvas,r=Math.min(2,globalThis.devicePixelRatio||1),i=Math.round(t.clientWidth*r),a=Math.round(t.clientHeight*r);i>0&&a>0&&(t.width!==i||t.height!==a)&&(t.width=i,t.height=a);let s=t.getContext(`2d`);if(m())f=P(f,d);else{let e=h.read();e.length>0&&(p.length!==25&&(p=oe(24,e.length)),d=ce(d,se(e,p)),f=P(f,d))}if(s){let e=getComputedStyle(document.documentElement);le(s,{width:t.width,height:t.height},d,f,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(w()){n.glyphs.textContent=d.map(N).join(``);let[t,r]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(C());let i=S();!u&&i>0&&(n.seek.value=String(Math.round(C()/i*1e3)))}requestAnimationFrame(B)}function V(e){n.video.hidden=!e}function H(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:y(),album:b(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void k()),navigator.mediaSession.setActionHandler(`pause`,()=>void k()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void A(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void A(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&D(n)}),n.prev.addEventListener(`click`,()=>void A(-1)),n.next.addEventListener(`click`,()=>void A(1)),n.stop.addEventListener(`click`,()=>void M()),n.playPause.addEventListener(`click`,()=>void k()),n.seek.addEventListener(`input`,()=>{u=!0}),n.seek.addEventListener(`change`,()=>{let e=S();e>0&&h.seek(Number(n.seek.value)/1e3*e),u=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;h.volume=e;try{localStorage.setItem(I,String(e))}catch{}});let U=e=>{e.addEventListener(`change`,()=>{let t=ee(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,R();return}x(i),i=t,a=0,r=`local`,g.close(),l=``,D(0)})};U(n.files),U(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=E(n.remoteUrl.value);if(t===``){l=`That is not an address.`,R();return}(async()=>{s=`connecting`,R();let e=ae(t);if(e){s=`error`,c=e,l=e,r=`local`,R();return}if(await j(t)===null){s=`error`,c=`no nixamp answered there`,r=`local`,R();return}r=`remote`,l=``;try{localStorage.setItem(F,t)}catch{}g.connect(t),R()})()});let W=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??[],fe(n.recent??[])}catch{n.directoryNote.textContent=`The directory is not answering. Type an address instead.`;return}if(e.length===0){n.directoryNote.textContent=`Nobody is streaming right now.`;return}n.directoryNote.textContent=`${e.length} live ${e.length===1?`stream`:`streams`}:`;for(let t of e){let e=document.createElement(`li`),r=document.createElement(`button`);r.type=`button`;let i=document.createElement(`span`);i.className=`name`,i.textContent=t.name;let a=document.createElement(`span`);a.className=`detail`;let o=[t.nowPlaying,`${t.tracks} tracks`].filter(Boolean);t.code&&o.push(t.callers?`☎ ${t.code} · ${t.callers} on the phone`:`☎ ${t.code}`),a.textContent=o.join(` · `),r.append(i,a),r.addEventListener(`click`,()=>{n.remoteUrl.value=t.url,n.directory.hidden=!0,n.remoteForm.requestSubmit()}),e.append(r),t.ownerId&&Z&&t.ownerId!==Z&&e.append(he(t.ownerId,t.name)),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),W()}let G=null,de=e=>{n.adminConnections.replaceChildren();let t=document.createElement(`tr`);for(let e of[`Where`,`Network`,`Kind`,`Client`,`Track`,`Sent`]){let n=document.createElement(`th`);n.textContent=e,t.append(n)}n.adminConnections.append(t);for(let t of e.slice(0,40)){let e=document.createElement(`tr`);t.endedAt!==null&&(e.className=`ended`);let r=[[t.address,``],[t.network,`network-${t.network}`],[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)}},K=async()=>{try{let e=await fetch(`/api/connections`);if(!e.ok)return;let t=await e.json();n.adminNote.textContent=`${t.active??0} listening now.`,de(t.connections??[])}catch{n.adminNote.textContent=`lost touch with the server`}},q=async()=>{let e=!1,t=null;try{let n=await fetch(`/api/admin`);if(n.ok){let r=await n.json();e=r.allowed===!0,t=r.as??null}}catch{e=!1}n.adminPanel.hidden=!e,G&&clearInterval(G),G=null,e&&(n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,K(),G=setInterval(()=>void K(),2e3))};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();t&&(async()=>{try{let e=await fetch(`/api/source`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t})}),r=await e.json();n.adminNote.textContent=e.ok?`Now serving ${t}.`:r.error??`that did not work`,e.ok&&(n.adminSource.value=``)}catch{n.adminNote.textContent=`could not reach the server`}})()});let J=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`},fe=e=>{n.recentList.replaceChildren();let t=Z?e.filter(e=>e.ownerId&&e.ownerId!==Z):[];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 ${J(e.endedAt)}`:`ended ${J(e.endedAt)}`,r.append(i,a),t.append(r,he(e.ownerId,e.name)),n.recentList.append(t)}},pe=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(`a`);o.className=`button`,o.textContent=`Open`,o.href=e.key?`${e.url}/s/${e.key}`:e.url,o.rel=`noreferrer`;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 pe()}catch{s.disabled=!1}})()}),t.append(r,o,s),n.serversList.append(t)}}catch{n.serversPanel.hidden=!0}},me=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}},he=(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),me())}catch{}finally{n.disabled=!1}})()}),n},ge=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},_e=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,ve=async()=>{if(!_e())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:ge(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}},ye=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{}},Y=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`}},be=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=_e()&&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 ve();n.notifyWeb.checked=e,await Y({wantsWeb:e});return}await ye(),await Y({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{Y({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 Y({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),Y({phone:n.notifyPhone.value.trim()})});let X=!1,Z=``,Q=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(be(),me(),pe()):(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}.`:X?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=X?`Create account`:`Sign in`,n.accountToggle.textContent=X?`I have one`:`Create one`,n.accountPassword.autocomplete=X?`new-password`:`current-password`};n.accountToggle.addEventListener(`click`,()=>{X=!X,Q(null)}),n.accountForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.accountEmail.value.trim(),r=n.accountPassword.value;(async()=>{n.accountSubmit.disabled=!0;try{let e=await fetch(`/api/v1/auth/${X?`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}Z=i.account?.id??``,n.accountPassword.value=``,Q(i.account?.email??t),q()}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{}Z=``,Q(null),q()})()}),(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)}})(),(async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();Z=e.ok?t.account?.id??``:``,Q(e.ok?t.account?.email??`you`:null)}catch{Z=``,Q(null)}})(),q(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}W(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{g.close(),r=`local`,s=`idle`,c=``,R()}),n.listenHere.addEventListener(`change`,()=>{r===`remote`&&(async()=>{n.listenHere.checked?(await g.send({type:`stop`}),await O(o.index)):h.stop(),R()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),k();return;case`s`:M();return;case`n`:case`ArrowRight`:A(1);return;case`p`:case`ArrowLeft`:A(-1);return;case`ArrowDown`:e.preventDefault(),D(Math.min(_()-1,v()+1));return;case`ArrowUp`:e.preventDefault(),D(Math.max(0,v()-1));return}});let $=null;globalThis.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),$=e,n.install.hidden=!1}),n.install.addEventListener(`click`,()=>{$?.prompt(),$=null,n.install.hidden=!0});try{let e=localStorage.getItem(I);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),h.volume=Number(e));let t=localStorage.getItem(F);t&&(n.remoteUrl.value=t)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await j(e)===null)return;let t=await ie(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,r=`remote`,l=``,g.connect(e),R())})(),R(),requestAnimationFrame(B)}R(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};