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.
- package/README.md +225 -0
- package/bin/nixamp.mjs +4 -1
- package/dist/accounts.d.ts +54 -0
- package/dist/accounts.js +160 -0
- package/dist/admin.d.ts +47 -0
- package/dist/admin.js +209 -0
- package/dist/broadcast.d.ts +96 -0
- package/dist/broadcast.js +193 -0
- package/dist/channels.d.ts +94 -0
- package/dist/channels.js +235 -0
- package/dist/connections.d.ts +72 -0
- package/dist/connections.js +128 -0
- package/dist/daemon.d.ts +39 -0
- package/dist/daemon.js +170 -0
- package/dist/directory.d.ts +63 -0
- package/dist/directory.js +111 -0
- package/dist/ingest.d.ts +80 -0
- package/dist/ingest.js +252 -0
- package/dist/main.js +103 -6
- package/dist/manage.js +30 -7
- package/dist/owner.d.ts +53 -0
- package/dist/owner.js +96 -0
- package/dist/paywall.d.ts +60 -0
- package/dist/paywall.js +162 -0
- package/dist/playlist.d.ts +16 -0
- package/dist/playlist.js +57 -2
- package/dist/publish.d.ts +36 -0
- package/dist/publish.js +90 -0
- package/dist/rtmp-in.d.ts +22 -0
- package/dist/rtmp-in.js +79 -0
- package/dist/server.d.ts +117 -4
- package/dist/server.js +923 -24
- package/dist/session.d.ts +29 -0
- package/dist/session.js +184 -0
- package/dist/share.d.ts +74 -0
- package/dist/share.js +172 -0
- package/dist/sources.d.ts +37 -0
- package/dist/sources.js +125 -0
- package/package.json +5 -2
- package/src/accounts.ts +193 -0
- package/src/admin.ts +243 -0
- package/src/broadcast.ts +264 -0
- package/src/channels.ts +281 -0
- package/src/connections.ts +158 -0
- package/src/daemon.ts +193 -0
- package/src/directory.ts +135 -0
- package/src/ingest.ts +297 -0
- package/src/main.ts +107 -6
- package/src/manage.ts +35 -7
- package/src/owner.ts +113 -0
- package/src/paywall.ts +198 -0
- package/src/playlist.ts +68 -2
- package/src/publish.ts +101 -0
- package/src/rtmp-in.ts +90 -0
- package/src/server.ts +1087 -23
- package/src/session.ts +209 -0
- package/src/share.ts +193 -0
- package/src/sources.ts +136 -0
- package/src/types/auth-system.d.ts +77 -0
- package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
- package/web/dist/assets/index-WYJ6R4uF.js +1 -0
- package/web/dist/index.html +37 -6
- package/web/dist/install.ps1 +214 -0
- package/web/dist/sw.js +3 -3
- 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(()=>{})});
|
package/web/dist/index.html
CHANGED
|
@@ -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-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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="
|
|
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-
|
|
2
|
+
const CACHE = "nixamp-1788901977701";
|
|
3
3
|
const PRECACHE = [
|
|
4
4
|
"/",
|
|
5
5
|
"/apple-touch-icon.png",
|
|
6
|
-
"/assets/index-
|
|
7
|
-
"/assets/index-
|
|
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(()=>{})});
|