nixamp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +225 -0
  2. package/bin/nixamp.mjs +4 -1
  3. package/dist/accounts.d.ts +54 -0
  4. package/dist/accounts.js +160 -0
  5. package/dist/admin.d.ts +47 -0
  6. package/dist/admin.js +209 -0
  7. package/dist/broadcast.d.ts +96 -0
  8. package/dist/broadcast.js +193 -0
  9. package/dist/channels.d.ts +94 -0
  10. package/dist/channels.js +235 -0
  11. package/dist/connections.d.ts +72 -0
  12. package/dist/connections.js +128 -0
  13. package/dist/daemon.d.ts +39 -0
  14. package/dist/daemon.js +170 -0
  15. package/dist/directory.d.ts +63 -0
  16. package/dist/directory.js +111 -0
  17. package/dist/ingest.d.ts +80 -0
  18. package/dist/ingest.js +252 -0
  19. package/dist/main.js +103 -6
  20. package/dist/manage.js +30 -7
  21. package/dist/owner.d.ts +53 -0
  22. package/dist/owner.js +96 -0
  23. package/dist/paywall.d.ts +60 -0
  24. package/dist/paywall.js +162 -0
  25. package/dist/playlist.d.ts +16 -0
  26. package/dist/playlist.js +57 -2
  27. package/dist/publish.d.ts +36 -0
  28. package/dist/publish.js +90 -0
  29. package/dist/rtmp-in.d.ts +22 -0
  30. package/dist/rtmp-in.js +79 -0
  31. package/dist/server.d.ts +117 -4
  32. package/dist/server.js +923 -24
  33. package/dist/session.d.ts +29 -0
  34. package/dist/session.js +184 -0
  35. package/dist/share.d.ts +74 -0
  36. package/dist/share.js +172 -0
  37. package/dist/sources.d.ts +37 -0
  38. package/dist/sources.js +125 -0
  39. package/package.json +5 -2
  40. package/src/accounts.ts +193 -0
  41. package/src/admin.ts +243 -0
  42. package/src/broadcast.ts +264 -0
  43. package/src/channels.ts +281 -0
  44. package/src/connections.ts +158 -0
  45. package/src/daemon.ts +193 -0
  46. package/src/directory.ts +135 -0
  47. package/src/ingest.ts +297 -0
  48. package/src/main.ts +107 -6
  49. package/src/manage.ts +35 -7
  50. package/src/owner.ts +113 -0
  51. package/src/paywall.ts +198 -0
  52. package/src/playlist.ts +68 -2
  53. package/src/publish.ts +101 -0
  54. package/src/rtmp-in.ts +90 -0
  55. package/src/server.ts +1087 -23
  56. package/src/session.ts +209 -0
  57. package/src/share.ts +193 -0
  58. package/src/sources.ts +136 -0
  59. package/src/types/auth-system.d.ts +77 -0
  60. package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
  61. package/web/dist/assets/index-WYJ6R4uF.js +1 -0
  62. package/web/dist/index.html +37 -6
  63. package/web/dist/install.ps1 +214 -0
  64. package/web/dist/sw.js +3 -3
  65. package/web/dist/assets/index-Dhja5wxB.js +0 -1
@@ -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=new Set([`mp3`,`flac`,`ogg`,`oga`,`opus`,`m4a`,`aac`,`wav`,`wma`,`aiff`,`aif`,`alac`,`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`]);function o(e,t=``){if(t.startsWith(`audio/`)||t.startsWith(`video/`))return!0;let n=e.lastIndexOf(`.`);return n>0&&a.has(e.slice(n+1).toLowerCase())}function s(e,t){return e.localeCompare(t,void 0,{numeric:!0,sensitivity:`base`})}function c(e){return e.filter(e=>o(e.name,e.type)).sort((e,t)=>s(l(e),l(t))).map(e=>({title:n(e.name),artist:``,album:u(l(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function l(e){return e.webkitRelativePath||e.name}function u(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function d(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var f=2048,p=class{elements;handlers;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(m(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=f,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.video?this.elements.video:this.elements.audio;n!==this.active&&(this.active.pause(),this.active.removeAttribute(`src`),this.active.load(),this.active=n),this.active.src=e.url,this.active.load(),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}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 m(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 h(){return{revision:0,tracks:[],index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function g(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 _(e,t){return`${e===``?``:g(e)}${t.startsWith(`/`)?t:`/${t}`}`}function v(e,t){return _(e,`/api/media/${t}`)}function y(e){if(typeof e!=`object`||!e)return null;let t=e;if(!Array.isArray(t.tracks))return null;let n=h(),r=(e,t)=>typeof e==`number`&&Number.isFinite(e)?e:t,i=Array.isArray(t.levels)?t.levels:[];return{revision:r(t.revision,0),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)}}),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 ee=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=g(e);this.close(),this.base=t,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let n=new EventSource(_(t,`/api/events`));this.source=n,n.onopen=()=>this.handlers.onStatus(`live`),n.onmessage=e=>{let t=y(b(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(_(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=y(await t.json());n&&this.handlers.onSnapshot(n)}media(e){return v(this.base,e)}close(){this.source?.close(),this.source=null}};function b(e){try{return JSON.parse(e)}catch{return null}}async function te(e,t){try{let n=await fetch(_(e,`/api/state`),{signal:t});return n.ok?y(await n.json()):null}catch{return null}}async function x(e,t){try{let n=await fetch(_(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}}var S=.14,C=.02;function ne(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 re(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 w(e,t,n=S){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function T(e,t,n=C){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function E(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 D=`nixamp.remote`,O=`nixamp.volume`;function k(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function A(){let n={status:k(`status`),source:k(`source`),install:k(`install`),video:k(`video`),audio:k(`audio`),title:k(`title-line`),album:k(`album-line`),elapsed:k(`elapsed`),total:k(`total`),seek:k(`seek`),canvas:k(`spectrum`),glyphs:k(`glyphs`),levels:k(`levels`),playlist:k(`playlist`),playlistTitle:k(`playlist-panel`),note:k(`note`),files:k(`files`),folder:k(`folder`),remoteUrl:k(`remote-url`),remoteForm:k(`remote-form`),remoteState:k(`remote-state`),disconnect:k(`disconnect`),browse:k(`browse`),accountForm:k(`account-form`),accountEmail:k(`account-email`),accountPassword:k(`account-password`),accountSubmit:k(`account-submit`),accountToggle:k(`account-toggle`),accountSignOut:k(`account-signout`),accountNote:k(`account-note`),adminPanel:k(`admin-panel`),adminNote:k(`admin-note`),adminConnections:k(`admin-connections`),adminRestream:k(`admin-restream`),adminSource:k(`admin-source`),directory:k(`directory`),directoryNote:k(`directory-note`),directoryList:k(`directory-list`),listenHere:k(`listen-here`),volume:k(`volume`),prev:k(`prev`),playPause:k(`play-pause`),stop:k(`stop`),next:k(`next`)},r=`local`,i=[],a=0,o=h(),s=`idle`,l=``,u=`Pick files, or connect to a nixamp running somewhere else.`,f=!1,m=Array(24).fill(0),_=Array(24).fill(0),v=[],y=()=>r===`remote`&&!n.listenHere.checked,b=new p({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),H()},onEnded:()=>z(1),onState:()=>H(),onError:e=>{u=e,H()}}),S=new ee({onSnapshot:e=>{o=e,y()&&(m=e.bars.length>0?e.bars:m,_=T(_,m)),H()},onStatus:(e,t)=>{s=e,l=t??``,H()}}),C=()=>r===`remote`?o.tracks.length:i.length,A=()=>r===`remote`?o.index:a,j=()=>{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.`},M=()=>(r===`remote`?o.tracks[o.index]:i[a])?.album||`—`,N=()=>y()?o.tracks[o.index]?.duration??0:b.duration,P=()=>y()?o.position:b.position,F=()=>y()?o.playing:b.playing;async function I(e){if(r===`remote`){if(y()){await S.send({type:`play`,index:e});return}await S.send({type:`select`,index:e}),await L(e);return}let t=i[e];t&&(a=e,await b.load(t,!0),ae(t.video),G(),H())}async function L(e){let t=o.tracks[e];t&&(await b.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:S.media(e),video:!1,objectUrl:!1},!0),G())}async function R(){if(y()){await S.send({type:`toggle`});return}C()!==0&&(b.playing?b.pause():b.position>0?await b.play():await I(A()),H())}async function z(e){let t=C();if(t!==0){if(y()){await S.send({type:e>0?`next`:`prev`});return}await I((A()+e+t)%t)}}async function B(){if(y()){await S.send({type:`stop`});return}b.stop(),m=Array(24).fill(0),_=[...m],H()}let V=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function H(){let t=C(),a=F();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=j(),n.album.textContent=M();let c=P(),d=N();n.elapsed.textContent=e(c),n.total.textContent=d>0?e(d):`--:--`,f||(n.seek.value=String(d>0?Math.round(c/d*1e3):0),n.seek.disabled=d<=0||y()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${S.address.replace(/^https?:\/\//,``)||`—`}`:i.length>0?`local · ${i.length} files`:`no source`,n.remoteState.textContent=r===`remote`?`${s}${l?` — ${l}`:``}`:`not connected`,n.remoteState.dataset.status=r===`remote`?s:`idle`,n.disconnect.hidden=r!==`remote`;let p=r===`remote`&&o.note!==``?o.note:u;n.note.textContent=p,n.note.hidden=p===``,ie(),n.glyphs.textContent=m.map(V).join(``);let[h,g]=y()?o.levels:b.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(h*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(g*6)).padEnd(6,`·`)}`}let U=``;function ie(){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!==U&&(U=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=A(),l=F();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 W(){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(y())_=T(_,m);else{let e=b.read();e.length>0&&(v.length!==25&&(v=ne(24,e.length)),m=w(m,re(e,v)),_=T(_,m))}if(s){let e=getComputedStyle(document.documentElement);E(s,{width:t.width,height:t.height},m,_,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(F()){n.glyphs.textContent=m.map(V).join(``);let[t,r]=y()?o.levels:b.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(P());let i=N();!f&&i>0&&(n.seek.value=String(Math.round(P()/i*1e3)))}requestAnimationFrame(W)}function ae(e){n.video.hidden=!e}function G(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:j(),album:M(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void R()),navigator.mediaSession.setActionHandler(`pause`,()=>void R()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void z(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void z(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&I(n)}),n.prev.addEventListener(`click`,()=>void z(-1)),n.next.addEventListener(`click`,()=>void z(1)),n.stop.addEventListener(`click`,()=>void B()),n.playPause.addEventListener(`click`,()=>void R()),n.seek.addEventListener(`input`,()=>{f=!0}),n.seek.addEventListener(`change`,()=>{let e=N();e>0&&b.seek(Number(n.seek.value)/1e3*e),f=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;b.volume=e;try{localStorage.setItem(O,String(e))}catch{}});let K=e=>{e.addEventListener(`change`,()=>{let t=c(Array.from(e.files??[]));if(t.length===0){u=`Nothing playable in that selection.`,H();return}d(i),i=t,a=0,r=`local`,S.close(),u=``,I(0)})};K(n.files),K(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=g(n.remoteUrl.value);if(t===``){u=`That is not an address.`,H();return}(async()=>{if(s=`connecting`,H(),await x(t)===null){s=`error`,l=`no nixamp answered there`,r=`local`,H();return}r=`remote`,u=``;try{localStorage.setItem(D,t)}catch{}S.connect(t),H()})()});let q=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));e=(await t.json()).streams??[]}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`,a.textContent=t.nowPlaying?`${t.nowPlaying} · ${t.tracks} tracks`:`${t.tracks} tracks`,r.append(i,a),r.addEventListener(`click`,()=>{n.remoteUrl.value=t.url,n.directory.hidden=!0,n.remoteForm.requestSubmit()}),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),q()}let J=null,oe=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)}},Y=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.`,oe(t.connections??[])}catch{n.adminNote.textContent=`lost touch with the server`}},X=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,J&&clearInterval(J),J=null,e&&(n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,Y(),J=setInterval(()=>void Y(),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 Z=!1,Q=e=>{let t=e!==null;n.accountForm.hidden=t,n.accountSignOut.hidden=!t,n.accountNote.textContent=t?`Signed in as ${e}.`:Z?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=Z?`Create account`:`Sign in`,n.accountToggle.textContent=Z?`I have one`:`Create one`,n.accountPassword.autocomplete=Z?`new-password`:`current-password`};n.accountToggle.addEventListener(`click`,()=>{Z=!Z,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/${Z?`signup`:`login`}`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({email:t,password:r})}),i=await e.json();if(!e.ok){n.accountNote.textContent=i.error??`that did not work`;return}n.accountPassword.value=``,Q(i.account?.email??t),X()}catch{n.accountNote.textContent=`could not reach nixamp.com`}finally{n.accountSubmit.disabled=!1}})()}),n.accountSignOut.addEventListener(`click`,()=>{(async()=>{try{await fetch(`/api/v1/auth/logout`,{method:`POST`})}catch{}Q(null),X()})()}),(async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();Q(e.ok?t.account?.email??`you`:null)}catch{Q(null)}})(),X(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}q(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{S.close(),r=`local`,s=`idle`,l=``,H()}),n.listenHere.addEventListener(`change`,()=>{r===`remote`&&(async()=>{n.listenHere.checked?(await S.send({type:`stop`}),await L(o.index)):b.stop(),H()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),R();return;case`s`:B();return;case`n`:case`ArrowRight`:z(1);return;case`p`:case`ArrowLeft`:z(-1);return;case`ArrowDown`:e.preventDefault(),I(Math.min(C()-1,A()+1));return;case`ArrowUp`:e.preventDefault(),I(Math.max(0,A()-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(O);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),b.volume=Number(e));let t=localStorage.getItem(D);t&&(n.remoteUrl.value=t)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await x(e)===null)return;let t=await te(e);t&&t.tracks.length!==0&&(n.remoteUrl.value=e,r=`remote`,u=``,S.connect(e),H())})(),H(),requestAnimationFrame(W)}A(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});
@@ -16,8 +16,8 @@
16
16
  <meta property="og:title" content="nixamp" />
17
17
  <meta property="og:description" content="It really whips the terminal's ass." />
18
18
  <meta property="og:type" content="website" />
19
- <script type="module" crossorigin src="/assets/index-Dhja5wxB.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BGKWWaIx.css">
19
+ <script type="module" crossorigin src="/assets/index-WYJ6R4uF.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-0wAv50Ay.css">
21
21
  </head>
22
22
  <body>
23
23
  <main id="app">
@@ -28,7 +28,7 @@
28
28
  <button id="install" type="button" class="ghost" hidden>Install</button>
29
29
  </header>
30
30
 
31
- <section class="panel" data-title="Now Playing">
31
+ <section class="panel player-only" data-title="Now Playing">
32
32
  <video id="video" playsinline hidden></video>
33
33
  <audio id="audio"></audio>
34
34
  <div id="title-line" class="track-title">Nothing loaded.</div>
@@ -41,7 +41,7 @@
41
41
  </section>
42
42
 
43
43
  <div class="split">
44
- <section class="panel" data-title="Spectrum Analyser">
44
+ <section class="panel player-only" data-title="Spectrum Analyser">
45
45
  <canvas id="spectrum" aria-hidden="true"></canvas>
46
46
  <div class="meters">
47
47
  <span id="glyphs" class="glyphs"></span>
@@ -66,7 +66,7 @@
66
66
  </section>
67
67
 
68
68
  <div class="split">
69
- <section class="panel" data-title="Files">
69
+ <section class="panel player-only" data-title="Files">
70
70
  <p class="hint">Nothing is uploaded. The browser decodes your files where they are.</p>
71
71
  <div class="picker">
72
72
  <label class="button">
@@ -80,13 +80,44 @@
80
80
  </div>
81
81
  </section>
82
82
 
83
- <section class="panel" data-title="Remote">
83
+ <section class="panel" data-title="Live now" id="directory" hidden>
84
+ <p class="hint" id="directory-note">Looking for live streams…</p>
85
+ <ul id="directory-list" class="directory-list"></ul>
86
+ <p class="hint"><a href="/" id="directory-back" hidden>Back to the player</a></p>
87
+ </section>
88
+
89
+ <section class="panel player-only" data-title="Account">
90
+ <p class="hint" id="account-note">Sign in to nixamp.com to publish and get paid.</p>
91
+ <form id="account-form" class="picker">
92
+ <input id="account-email" type="email" autocomplete="email"
93
+ placeholder="you@example.com" aria-label="Email" />
94
+ <input id="account-password" type="password" autocomplete="current-password"
95
+ placeholder="password" aria-label="Password" />
96
+ <button type="submit" class="button" id="account-submit">Sign in</button>
97
+ <button id="account-toggle" type="button" class="ghost">Create one</button>
98
+ <button id="account-signout" type="button" class="ghost" hidden>Sign out</button>
99
+ </form>
100
+ </section>
101
+
102
+ <section class="panel player-only" data-title="Admin" id="admin-panel" hidden>
103
+ <p class="hint" id="admin-note">Checking…</p>
104
+ <table class="admin-table" id="admin-connections"></table>
105
+ <form id="admin-restream" class="picker">
106
+ <input id="admin-source" type="text" placeholder="a URL or a path to re-stream"
107
+ aria-label="Source to re-stream" />
108
+ <button type="submit" class="button">Re-stream</button>
109
+ </form>
110
+ </section>
111
+
112
+ <section class="panel player-only" data-title="Remote">
84
113
  <p class="hint">Run <code>nixamp serve ~/Music --host 0.0.0.0</code> and drive it from here.</p>
85
114
  <form id="remote-form" class="picker">
86
115
  <input id="remote-url" type="text" inputmode="url" placeholder="192.168.1.7:4321" aria-label="nixamp server address" />
87
116
  <button type="submit" class="button">Connect</button>
88
117
  <button id="disconnect" type="button" class="ghost" hidden>Disconnect</button>
118
+ <button id="browse" type="button" class="ghost">Browse the directory</button>
89
119
  </form>
120
+
90
121
  <label class="check">
91
122
  <input id="listen-here" type="checkbox" />
92
123
  Listen on this device
@@ -0,0 +1,214 @@
1
+ <#
2
+ .SYNOPSIS
3
+ nixamp installer for Windows.
4
+
5
+ .DESCRIPTION
6
+ irm https://nixamp.com/install.ps1 | iex
7
+
8
+ Installs the app and the CLI under %LOCALAPPDATA%\nixamp. No administrator
9
+ rights, no registry, nothing outside your profile. The CLI runs on the Node
10
+ inside the app bundle, so no system Node is needed.
11
+
12
+ Updating is `nixamp update` and removing is `nixamp uninstall`, which runs a
13
+ script this installer leaves behind.
14
+
15
+ .PARAMETER CliOnly
16
+ Never install the app. The CLI then needs Node 24 or newer on PATH.
17
+
18
+ .PARAMETER Version
19
+ Install a specific release instead of the latest.
20
+
21
+ .PARAMETER Prefix
22
+ Install root. Defaults to $env:LOCALAPPDATA\nixamp.
23
+
24
+ .EXAMPLE
25
+ & ([scriptblock]::Create((irm https://nixamp.com/install.ps1))) -CliOnly
26
+ #>
27
+ [CmdletBinding()]
28
+ param(
29
+ [switch]$CliOnly,
30
+ [string]$Version = $env:NIXAMP_VERSION,
31
+ [string]$Prefix = $(if ($env:NIXAMP_PREFIX) { $env:NIXAMP_PREFIX } else { Join-Path $env:LOCALAPPDATA 'nixamp' })
32
+ )
33
+
34
+ $ErrorActionPreference = 'Stop'
35
+ $ProgressPreference = 'SilentlyContinue'
36
+
37
+ $Repo = 'profullstack/nixamp'
38
+ $Site = if ($env:NIXAMP_SITE) { $env:NIXAMP_SITE } else { 'https://nixamp.com' }
39
+
40
+ function Fail($message) {
41
+ Write-Error "nixamp: $message"
42
+ exit 1
43
+ }
44
+
45
+ # arm64 Windows exists and Electron ships for it, so do not assume x64.
46
+ $Arch = switch ($env:PROCESSOR_ARCHITECTURE) {
47
+ 'AMD64' { 'x64' }
48
+ 'ARM64' { 'arm64' }
49
+ 'x86' { Fail 'nixamp does not ship a 32-bit build.' }
50
+ default { 'x64' }
51
+ }
52
+
53
+ if (-not $Version) {
54
+ try {
55
+ $latest = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest"
56
+ $Version = $latest.tag_name -replace '^v', ''
57
+ } catch {
58
+ Fail "could not determine the latest version. Pass -Version, or see $Site"
59
+ }
60
+ }
61
+
62
+ $Base = if ($env:NIXAMP_RELEASE_BASE) {
63
+ $env:NIXAMP_RELEASE_BASE
64
+ } else {
65
+ "https://github.com/$Repo/releases/download/v$Version"
66
+ }
67
+
68
+ $Bin = Join-Path $Prefix 'bin'
69
+ $Share = Join-Path $Prefix 'share'
70
+
71
+ Write-Host "nixamp $Version"
72
+ Write-Host " platform: windows-$Arch"
73
+ Write-Host " desktop: $(if ($CliOnly) { 'no' } else { 'yes' })"
74
+ Write-Host " prefix: $Prefix"
75
+ Write-Host ""
76
+
77
+ $work = Join-Path ([System.IO.Path]::GetTempPath()) ("nixamp-" + [guid]::NewGuid().ToString('N'))
78
+ New-Item -ItemType Directory -Force -Path $work, $Bin, $Share | Out-Null
79
+
80
+ $paths = New-Object System.Collections.Generic.List[string]
81
+ $paths.Add((Join-Path $Bin 'nixamp.cmd'))
82
+ $paths.Add($Share)
83
+ $method = 'cli-tarball'
84
+ $appDir = Join-Path $Share 'app'
85
+ $cliDir = $null
86
+ $runtime = $null
87
+
88
+ # --- the app ------------------------------------------------------------------
89
+
90
+ if (-not $CliOnly) {
91
+ $asset = "nixamp-$Version-win-$Arch.zip"
92
+ Write-Host 'Downloading the app...'
93
+ try {
94
+ Invoke-WebRequest "$Base/$asset" -OutFile (Join-Path $work 'app.zip')
95
+ if (Test-Path $appDir) { Remove-Item -Recurse -Force $appDir }
96
+ New-Item -ItemType Directory -Force -Path $appDir | Out-Null
97
+ Expand-Archive -Path (Join-Path $work 'app.zip') -DestinationPath $appDir -Force
98
+ $method = 'windows-app'
99
+ $cliDir = Join-Path $appDir 'resources\cli'
100
+ $runtime = Join-Path $appDir 'nixamp.exe'
101
+
102
+ # A Start menu shortcut, which is what the NSIS installer would give you,
103
+ # done without touching anything outside the profile.
104
+ $menu = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\nixamp.lnk'
105
+ $shell = New-Object -ComObject WScript.Shell
106
+ $link = $shell.CreateShortcut($menu)
107
+ $link.TargetPath = $runtime
108
+ $link.WorkingDirectory = $appDir
109
+ $link.Description = "It really whips the terminal's ass."
110
+ $link.Save()
111
+ $paths.Add($menu)
112
+ } catch {
113
+ Write-Host " the app could not be installed ($($_.Exception.Message)); installing the CLI only."
114
+ $CliOnly = $true
115
+ $method = 'cli-tarball'
116
+ }
117
+ }
118
+
119
+ # --- the CLI ------------------------------------------------------------------
120
+
121
+ if ($method -eq 'cli-tarball') {
122
+ # Pure JavaScript, so one bundle runs everywhere a Node does.
123
+ $asset = "nixamp-cli-$Version.tar.gz"
124
+ Write-Host 'Downloading the CLI...'
125
+ $tarball = Join-Path $work 'cli.tar.gz'
126
+ try {
127
+ Invoke-WebRequest "$Base/$asset" -OutFile $tarball
128
+ } catch {
129
+ Fail "could not download $Base/$asset"
130
+ }
131
+
132
+ $cliDir = Join-Path $Share 'cli'
133
+ if (Test-Path $cliDir) { Remove-Item -Recurse -Force $cliDir }
134
+ New-Item -ItemType Directory -Force -Path $cliDir | Out-Null
135
+ # bsdtar has shipped in Windows since 1809 and reads gzip, so there is no
136
+ # third-party unpacker to ask for.
137
+ tar -xzf $tarball -C $cliDir --strip-components=1
138
+ if ($LASTEXITCODE -ne 0) { Fail 'could not unpack the CLI.' }
139
+
140
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
141
+ Write-Host ' note: no app was installed, so the CLI needs Node 24 or newer. It was not found.'
142
+ }
143
+ }
144
+
145
+ # The shim, written here because only the installer knows which of the two
146
+ # runtimes this machine ended up with.
147
+ $entry = Join-Path $cliDir 'bin\nixamp.mjs'
148
+ $shim = Join-Path $Bin 'nixamp.cmd'
149
+ if ($runtime) {
150
+ @"
151
+ @echo off
152
+ rem nixamp. Runs on the Node inside the app, so no system Node is required.
153
+ rem Written by the installer; ``nixamp uninstall`` removes it.
154
+ set NIXAMP_HOME=$Share
155
+ set ELECTRON_RUN_AS_NODE=1
156
+ "$runtime" "$entry" %*
157
+ "@ | Set-Content -Path $shim -Encoding ASCII
158
+ } else {
159
+ @"
160
+ @echo off
161
+ rem nixamp. Written by the installer; ``nixamp uninstall`` removes it.
162
+ set NIXAMP_HOME=$Share
163
+ node "$entry" %*
164
+ "@ | Set-Content -Path $shim -Encoding ASCII
165
+ }
166
+
167
+ # --- what was installed, and how to remove it ---------------------------------
168
+
169
+ $manifest = [ordered]@{
170
+ version = $Version
171
+ method = $method
172
+ installer = "$Site/install.ps1"
173
+ installedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
174
+ prefix = $Prefix
175
+ desktop = ($method -eq 'windows-app')
176
+ paths = $paths.ToArray()
177
+ }
178
+ $manifest | ConvertTo-Json | Set-Content -Path (Join-Path $Share 'manifest.json') -Encoding UTF8
179
+
180
+ $removals = ($paths | ForEach-Object { "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '$_'" }) -join "`n"
181
+ @"
182
+ # Removes nixamp. Written by the installer, which knew exactly what it created.
183
+ # Your music is NOT touched.
184
+ $removals
185
+ Write-Host 'nixamp removed.'
186
+ "@ | Set-Content -Path (Join-Path $Share 'uninstall.ps1') -Encoding UTF8
187
+
188
+ Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
189
+
190
+ # --- PATH ---------------------------------------------------------------------
191
+
192
+ $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
193
+ if ($userPath -notlike "*$Bin*") {
194
+ [Environment]::SetEnvironmentVariable('Path', "$userPath;$Bin", 'User')
195
+ Write-Host ""
196
+ Write-Host "Added $Bin to your PATH. Open a new terminal for it to take effect."
197
+ }
198
+
199
+ Write-Host ""
200
+ Write-Host "Installed nixamp $Version"
201
+ Write-Host " $(if ($method -eq 'windows-app') { 'app and CLI' } else { 'CLI only' })"
202
+
203
+ # ffmpeg decodes every track. Saying so now beats a confusing failure on first
204
+ # use, when the playlist loads and nothing comes out of the speakers.
205
+ if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
206
+ Write-Host ""
207
+ Write-Host ' ffmpeg was not found, and nixamp decodes with ffmpeg.'
208
+ Write-Host ' winget install Gyan.FFmpeg'
209
+ }
210
+
211
+ Write-Host ""
212
+ Write-Host 'Try: nixamp %USERPROFILE%\Music'
213
+ Write-Host 'Update with `nixamp update`, remove with `nixamp uninstall`.'
214
+ Write-Host "Docs: $Site"
package/web/dist/sw.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1788892198880";
2
+ const CACHE = "nixamp-1788901977701";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/apple-touch-icon.png",
6
- "/assets/index-BGKWWaIx.css",
7
- "/assets/index-Dhja5wxB.js",
6
+ "/assets/index-0wAv50Ay.css",
7
+ "/assets/index-WYJ6R4uF.js",
8
8
  "/icons/icon-192-maskable.png",
9
9
  "/icons/icon-192.png",
10
10
  "/icons/icon-512-maskable.png",
@@ -1 +0,0 @@
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=new Set([`mp3`,`flac`,`ogg`,`oga`,`opus`,`m4a`,`aac`,`wav`,`wma`,`aiff`,`aif`,`alac`,`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`]);function o(e,t=``){if(t.startsWith(`audio/`)||t.startsWith(`video/`))return!0;let n=e.lastIndexOf(`.`);return n>0&&a.has(e.slice(n+1).toLowerCase())}function s(e,t){return e.localeCompare(t,void 0,{numeric:!0,sensitivity:`base`})}function c(e){return e.filter(e=>o(e.name,e.type)).sort((e,t)=>s(l(e),l(t))).map(e=>({title:n(e.name),artist:``,album:u(l(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function l(e){return e.webkitRelativePath||e.name}function u(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function d(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var f=2048,p=class{elements;handlers;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(m(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=f,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.video?this.elements.video:this.elements.audio;n!==this.active&&(this.active.pause(),this.active.removeAttribute(`src`),this.active.load(),this.active=n),this.active.src=e.url,this.active.load(),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}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 m(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 h(){return{revision:0,tracks:[],index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function g(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 _(e,t){return`${e===``?``:g(e)}${t.startsWith(`/`)?t:`/${t}`}`}function v(e,t){return _(e,`/api/media/${t}`)}function y(e){if(typeof e!=`object`||!e)return null;let t=e;if(!Array.isArray(t.tracks))return null;let n=h(),r=(e,t)=>typeof e==`number`&&Number.isFinite(e)?e:t,i=Array.isArray(t.levels)?t.levels:[];return{revision:r(t.revision,0),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)}}),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 b=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=g(e);this.close(),this.base=t,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let n=new EventSource(_(t,`/api/events`));this.source=n,n.onopen=()=>this.handlers.onStatus(`live`),n.onmessage=e=>{let t=y(x(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(_(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=y(await t.json());n&&this.handlers.onSnapshot(n)}media(e){return v(this.base,e)}close(){this.source?.close(),this.source=null}};function x(e){try{return JSON.parse(e)}catch{return null}}async function S(e,t){try{let n=await fetch(_(e,`/api/state`),{signal:t});return n.ok?y(await n.json()):null}catch{return null}}async function C(e,t){try{let n=await fetch(_(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}}var w=.14,T=.02;function E(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 D(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 O(e,t,n=w){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function k(e,t,n=T){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function A(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 j=`nixamp.remote`,M=`nixamp.volume`;function N(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function P(){let n={status:N(`status`),source:N(`source`),install:N(`install`),video:N(`video`),audio:N(`audio`),title:N(`title-line`),album:N(`album-line`),elapsed:N(`elapsed`),total:N(`total`),seek:N(`seek`),canvas:N(`spectrum`),glyphs:N(`glyphs`),levels:N(`levels`),playlist:N(`playlist`),playlistTitle:N(`playlist-panel`),note:N(`note`),files:N(`files`),folder:N(`folder`),remoteUrl:N(`remote-url`),remoteForm:N(`remote-form`),remoteState:N(`remote-state`),disconnect:N(`disconnect`),listenHere:N(`listen-here`),volume:N(`volume`),prev:N(`prev`),playPause:N(`play-pause`),stop:N(`stop`),next:N(`next`)},r=`local`,i=[],a=0,o=h(),s=`idle`,l=``,u=`Pick files, or connect to a nixamp running somewhere else.`,f=!1,m=Array(24).fill(0),_=Array(24).fill(0),v=[],y=()=>r===`remote`&&!n.listenHere.checked,x=new p({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),K()},onEnded:()=>U(1),onState:()=>K(),onError:e=>{u=e,K()}}),w=new b({onSnapshot:e=>{o=e,y()&&(m=e.bars.length>0?e.bars:m,_=k(_,m)),K()},onStatus:(e,t)=>{s=e,l=t??``,K()}}),T=()=>r===`remote`?o.tracks.length:i.length,P=()=>r===`remote`?o.index:a,F=()=>{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.`},I=()=>(r===`remote`?o.tracks[o.index]:i[a])?.album||`—`,L=()=>y()?o.tracks[o.index]?.duration??0:x.duration,R=()=>y()?o.position:x.position,z=()=>y()?o.playing:x.playing;async function B(e){if(r===`remote`){if(y()){await w.send({type:`play`,index:e});return}await w.send({type:`select`,index:e}),await V(e);return}let t=i[e];t&&(a=e,await x.load(t,!0),X(t.video),Z(),K())}async function V(e){let t=o.tracks[e];t&&(await x.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:w.media(e),video:!1,objectUrl:!1},!0),Z())}async function H(){if(y()){await w.send({type:`toggle`});return}T()!==0&&(x.playing?x.pause():x.position>0?await x.play():await B(P()),K())}async function U(e){let t=T();if(t!==0){if(y()){await w.send({type:e>0?`next`:`prev`});return}await B((P()+e+t)%t)}}async function W(){if(y()){await w.send({type:`stop`});return}x.stop(),m=Array(24).fill(0),_=[...m],K()}let G=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function K(){let t=T(),a=z();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=F(),n.album.textContent=I();let c=R(),d=L();n.elapsed.textContent=e(c),n.total.textContent=d>0?e(d):`--:--`,f||(n.seek.value=String(d>0?Math.round(c/d*1e3):0),n.seek.disabled=d<=0||y()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${w.address.replace(/^https?:\/\//,``)||`—`}`:i.length>0?`local · ${i.length} files`:`no source`,n.remoteState.textContent=r===`remote`?`${s}${l?` — ${l}`:``}`:`not connected`,n.remoteState.dataset.status=r===`remote`?s:`idle`,n.disconnect.hidden=r!==`remote`;let p=r===`remote`&&o.note!==``?o.note:u;n.note.textContent=p,n.note.hidden=p===``,J(),n.glyphs.textContent=m.map(G).join(``);let[h,g]=y()?o.levels:x.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(h*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(g*6)).padEnd(6,`·`)}`}let q=``;function J(){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!==q&&(q=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=P(),l=z();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 Y(){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(y())_=k(_,m);else{let e=x.read();e.length>0&&(v.length!==25&&(v=E(24,e.length)),m=O(m,D(e,v)),_=k(_,m))}if(s){let e=getComputedStyle(document.documentElement);A(s,{width:t.width,height:t.height},m,_,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(z()){n.glyphs.textContent=m.map(G).join(``);let[t,r]=y()?o.levels:x.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(R());let i=L();!f&&i>0&&(n.seek.value=String(Math.round(R()/i*1e3)))}requestAnimationFrame(Y)}function X(e){n.video.hidden=!e}function Z(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:F(),album:I(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void H()),navigator.mediaSession.setActionHandler(`pause`,()=>void H()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void U(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void U(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&B(n)}),n.prev.addEventListener(`click`,()=>void U(-1)),n.next.addEventListener(`click`,()=>void U(1)),n.stop.addEventListener(`click`,()=>void W()),n.playPause.addEventListener(`click`,()=>void H()),n.seek.addEventListener(`input`,()=>{f=!0}),n.seek.addEventListener(`change`,()=>{let e=L();e>0&&x.seek(Number(n.seek.value)/1e3*e),f=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;x.volume=e;try{localStorage.setItem(M,String(e))}catch{}});let Q=e=>{e.addEventListener(`change`,()=>{let t=c(Array.from(e.files??[]));if(t.length===0){u=`Nothing playable in that selection.`,K();return}d(i),i=t,a=0,r=`local`,w.close(),u=``,B(0)})};Q(n.files),Q(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=g(n.remoteUrl.value);if(t===``){u=`That is not an address.`,K();return}(async()=>{if(s=`connecting`,K(),await C(t)===null){s=`error`,l=`no nixamp answered there`,r=`local`,K();return}r=`remote`,u=``;try{localStorage.setItem(j,t)}catch{}w.connect(t),K()})()}),n.disconnect.addEventListener(`click`,()=>{w.close(),r=`local`,s=`idle`,l=``,K()}),n.listenHere.addEventListener(`change`,()=>{r===`remote`&&(async()=>{n.listenHere.checked?(await w.send({type:`stop`}),await V(o.index)):x.stop(),K()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),H();return;case`s`:W();return;case`n`:case`ArrowRight`:U(1);return;case`p`:case`ArrowLeft`:U(-1);return;case`ArrowDown`:e.preventDefault(),B(Math.min(T()-1,P()+1));return;case`ArrowUp`:e.preventDefault(),B(Math.max(0,P()-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(M);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),x.volume=Number(e));let t=localStorage.getItem(j);t&&(n.remoteUrl.value=t)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await C(e)===null)return;let t=await S(e);t&&t.tracks.length!==0&&(n.remoteUrl.value=e,r=`remote`,u=``,w.connect(e),K())})(),K(),requestAnimationFrame(Y)}P(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});