nixamp 0.48.0 → 0.49.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 (56) hide show
  1. package/dist/ai-keys.d.ts +1 -1
  2. package/dist/ai-keys.js +2 -0
  3. package/dist/ai-label.d.ts +210 -0
  4. package/dist/ai-label.js +725 -0
  5. package/dist/ai-radio.d.ts +11 -5
  6. package/dist/ai-radio.js +27 -11
  7. package/dist/ai-text.d.ts +2 -0
  8. package/dist/ai-text.js +4 -2
  9. package/dist/label-routes.d.ts +15 -0
  10. package/dist/label-routes.js +255 -0
  11. package/dist/server.d.ts +3 -0
  12. package/dist/server.js +28 -1
  13. package/dist/station-cli.js +109 -0
  14. package/dist/station-guide.d.ts +1 -1
  15. package/dist/station-guide.js +8 -3
  16. package/dist/station-mcp.d.ts +247 -20
  17. package/dist/station-mcp.js +36 -0
  18. package/dist/station-metering.js +2 -2
  19. package/dist/station-usage.d.ts +1 -1
  20. package/dist/station-usage.js +15 -1
  21. package/package.json +1 -1
  22. package/src/ai-keys.ts +2 -0
  23. package/src/ai-label.ts +826 -0
  24. package/src/ai-radio.ts +30 -13
  25. package/src/ai-text.ts +6 -2
  26. package/src/label-routes.ts +268 -0
  27. package/src/server.ts +30 -1
  28. package/src/station-cli.ts +102 -0
  29. package/src/station-guide.ts +8 -3
  30. package/src/station-mcp.ts +34 -0
  31. package/src/station-metering.ts +2 -2
  32. package/src/station-usage.ts +15 -1
  33. package/web/dist/assets/{ar-ropwGSSV.js → ar-BEVjqkqM.js} +1 -1
  34. package/web/dist/assets/{cs-Bi7uRYjd.js → cs-DjazXQ15.js} +1 -1
  35. package/web/dist/assets/{da-B8OELPch.js → da-DnyF6SYK.js} +1 -1
  36. package/web/dist/assets/{de-DJOTeXET.js → de--q1ph9Ez.js} +1 -1
  37. package/web/dist/assets/{es-BIupzuzS.js → es-Do3DkBwD.js} +1 -1
  38. package/web/dist/assets/{fi-A6MoWkzZ.js → fi-B7sJTx1N.js} +1 -1
  39. package/web/dist/assets/{fr-DerIDIwl.js → fr-Dy7f5rGi.js} +1 -1
  40. package/web/dist/assets/{hi-CyUtRBH4.js → hi-C9vSnTQ4.js} +1 -1
  41. package/web/dist/assets/{hu-DD1vrIhY.js → hu-h-eueQE8.js} +1 -1
  42. package/web/dist/assets/{id-C6OAMzii.js → id-D8Ffd-Pc.js} +1 -1
  43. package/web/dist/assets/{it-Xy86vmU7.js → it-BKM_2rj1.js} +1 -1
  44. package/web/dist/assets/main-Dl_uRj22.js +22 -0
  45. package/web/dist/assets/{nl-4Q-K2Jf1.js → nl-Cj3ETrcl.js} +1 -1
  46. package/web/dist/assets/radio-zPgyISLf.js +1 -0
  47. package/web/dist/assets/{ru-CpxwOhC4.js → ru-B5zmGSnG.js} +1 -1
  48. package/web/dist/assets/{sv-aRvu7MWa.js → sv-BMtjA1uI.js} +1 -1
  49. package/web/dist/assets/{uk-BCwwfngg.js → uk-B2vuB6Un.js} +1 -1
  50. package/web/dist/assets/{vi-tvWPiAH_.js → vi-CXam3lDh.js} +1 -1
  51. package/web/dist/assets/{zh-Bp2aLEyX.js → zh-DnYhyzqb.js} +1 -1
  52. package/web/dist/index.html +1 -1
  53. package/web/dist/radio.html +33 -1
  54. package/web/dist/sw.js +20 -20
  55. package/web/dist/assets/main-CuG8-ADK.js +0 -22
  56. package/web/dist/assets/radio-DI0H8flN.js +0 -1
@@ -1 +0,0 @@
1
- import{n as e}from"./chunk-Q2XQMA4B-CDsJ8fBU.js";import{l as t}from"./remote-DJqvFdE6.js";var n=e=>document.querySelector(e);function r(e,t={},...n){let r=document.createElement(e);for(let[e,n]of Object.entries(t))n!==!1&&(e===`class`?r.className=String(n):r.setAttribute(e,n===!0?``:n));for(let e of n)e!=null&&e!==!1&&r.append(typeof e==`string`?document.createTextNode(e):e);return r}function i(e){n(`#sr-status`).textContent=e}var a;function o(e,t=`error`){let i=n(`#notice`);if(clearTimeout(a),i.classList.toggle(`ok`,t===`ok`),i.setAttribute(`role`,t===`ok`?`status`:`alert`),i.replaceChildren(),e!==``){let t=r(`button`,{type:`button`,class:`ghost notice-close`,"aria-label":`Dismiss`},`×`);t.addEventListener(`click`,()=>o(``)),i.append(r(`span`,{},e),t)}i.hidden=e===``,t===`ok`&&e&&(a=setTimeout(()=>o(``),6e3))}var s=class extends Error{status;code;constructor(e,t,n){super(n),this.status=e,this.code=t}},c={id:`here`,name:location.host,base:``,session:``,key:``},l=0;function u(e,t=!1){if(!c.base)return e;let n=new URL(c.base+e);return c.session?t&&n.searchParams.set(`session`,c.session):c.key&&n.searchParams.set(`k`,c.key),n.href}async function d(e,t,n){let r=c.base!==``,i=await fetch(u(t),{method:e,credentials:r?`omit`:`same-origin`,headers:{...n===void 0?{}:{"content-type":`application/json`},...r&&c.session?{authorization:`Bearer ${c.session}`}:{}},...n===void 0?{}:{body:JSON.stringify(n)}}),a=await i.json().catch(()=>({}));if(!i.ok)throw new s(i.status,String(a.code??i.status),String(a.error??`HTTP ${i.status}`));return a}async function f(e,t,n){if(!n?.disabled){n&&(n.disabled=!0);try{await t()}catch(t){o(`${e} failed: ${t instanceof Error?t.message:String(t)}`)}finally{n&&(n.disabled=!1)}}}function p(e){return e?`${Math.floor(e/60)}:${String(Math.floor(e%60)).padStart(2,`0`)}`:``}var m={live:`Live`,starting:`Starting`,recovering:`Coming back`,offline:`Offline`,stopped:`Off the air`},h=[],g=null;async function _(){let e=l,t=await d(`GET`,`/api/v1/stations?limit=200`);e===l&&(h=t.stations,D())}var v=`https://backtoschool.help`,y=!1,b=new Map;function x(e){try{let t=new URL(e);return`${t.origin}${t.pathname.replace(/\/+$/,``)}?t=${t.searchParams.get(`t`)??``}`}catch{return e}}async function S(){if(!y)return;let e=await fetch(`/api/v1/events?mine=true&kind=class&status=live&limit=100`,{credentials:`same-origin`});if(!e.ok)return;let t=await e.json().catch(()=>({}));b=new Map((t.events??[]).filter(e=>e.broadcastUrl).map(e=>[x(e.broadcastUrl),e]))}async function C(e,t){let n=await fetch(e,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:JSON.stringify(t)}),r=await n.json().catch(()=>({}));if(!n.ok)throw Error(String(r.error??`HTTP ${n.status}`));return r}async function w(e){e.desired!==`running`&&await d(`POST`,`/api/v1/stations/${e.id}/start`,{})}async function T(e,t){if(t&&!e.rights){O(e,{visibility:`public`,listed:!0});return}await d(`POST`,`/api/v1/stations/${e.id}/access`,t?{visibility:`public`,listed:!0}:{visibility:e.visibility,listed:!1}),t&&await w(e),i(t?`${e.name} is on live.streaming`:`${e.name} is off live.streaming`),await _()}async function ee(e,t){let n=b.get(x(e.links.page));if(!t){n&&await C(`/api/v1/events/${encodeURIComponent(n.id)}/end`,{version:n.version}),i(`${e.name} is off backtoschool.help`),await S(),D();return}let r=e;e.visibility===`private`&&(r=(await d(`POST`,`/api/v1/stations/${e.id}/access`,{visibility:`unlisted`})).station),await w(r);let a=(await C(`/api/v1/events`,{kind:`class`,title:r.name,broadcastUrl:r.links.page,visibility:`public`})).event;await C(`/api/v1/events/${encodeURIComponent(a.id)}/start`,{version:a.version}),i(`${r.name} is live on backtoschool.help`),await S(),await _()}async function te(e){let t=b.get(x(e.links.page));t&&await C(`/api/v1/events/${encodeURIComponent(t.id)}/end`,{version:t.version}).catch(()=>void 0)}function E(e){let t=r(`div`,{class:`targets`}),n=r(`input`,{type:`checkbox`});n.checked=e.visibility===`public`&&e.listed,n.addEventListener(`change`,()=>{n.disabled=!0,f(`live.streaming`,()=>T(e,n.checked)).finally(()=>{n.disabled=!1,_().catch(()=>void 0)})}),t.append(r(`label`,{title:`Public and listed in the live.streaming guide`},n,`On live.streaming`)),n.checked&&t.append(r(`a`,{href:`https://live.streaming/`,target:`_blank`,rel:`noopener`},`open`));let i=b.get(x(e.links.page)),a=r(`input`,{type:`checkbox`,disabled:!y});return a.checked=i!==void 0,a.addEventListener(`change`,()=>{a.disabled=!0,f(`backtoschool.help`,()=>ee(e,a.checked)).finally(()=>{a.disabled=!y,D()})}),t.append(r(`label`,{title:y?`A live class on backtoschool.help that plays this station`:`Open nixamp.com/radio signed in to put stations on backtoschool.help`},a,`On backtoschool.help`)),i&&t.append(r(`a`,{href:`${v}/live/${encodeURIComponent(i.slug)}`,target:`_blank`,rel:`noopener`},`open`)),t}function D(){let e=n(`#station-list`);if(e.replaceChildren(),h.length===0){e.append(r(`p`,{class:`hint`},`No stations yet. Make one below from a folder or a playlist.`));return}for(let t of h){let n=t.desired===`running`,a=t.visibility!==`private`,o=r(`button`,{type:`button`,class:`ghost`,disabled:!a,title:a?`Copy the listening link`:`Private: make it unlisted or public to share`},`Copy link`);o.addEventListener(`click`,()=>void f(`Copy`,async()=>{await navigator.clipboard.writeText(t.links.page),i(`Link to ${t.name} copied`),o.textContent=`Copied`,setTimeout(()=>{o.textContent=`Copy link`},1500)}));let s=r(`button`,{type:`button`},n?`Stop`:`Start`);s.addEventListener(`click`,()=>void f(n?`Stop`:`Start`,async()=>{await d(`POST`,`/api/v1/stations/${t.id}/${n?`stop`:`start`}`,{}),i(`${t.name} ${n?`stopped`:`started`}`),await _()}));let c=r(`button`,{type:`button`,class:`ghost`,disabled:t.state!==`live`},`Skip`);c.addEventListener(`click`,()=>void f(`Skip`,async()=>{await d(`POST`,`/api/v1/stations/${t.id}/skip`,{}),i(`Skipped`)}));let l=r(`button`,{type:`button`,class:`ghost`},`Who can listen…`);l.addEventListener(`click`,()=>O(t));let u=r(`button`,{type:`button`,class:`ghost danger`},`Delete`);u.addEventListener(`click`,()=>void f(`Delete`,async()=>{confirm(`Delete the station "${t.name}"? Its files are not touched.`)&&(await d(`DELETE`,`/api/v1/stations/${t.id}`),i(`${t.name} deleted`),await _())}));let p=t.excluded.length?r(`button`,{type:`button`,class:`ghost`},`Retry ${t.excluded.length} left out`):null;p?.addEventListener(`click`,()=>void f(`Retry`,async()=>{await d(`POST`,`/api/v1/stations/${t.id}/retry`,{}),await _()})),e.append(r(`article`,{class:`station`,id:`station-${t.id}`,"aria-label":t.name},r(`div`,{class:`station-head`},r(`span`,{class:`state state-${t.state}`},m[t.state]??t.state),r(`h3`,{},t.name),r(`span`,{class:`muted`},`${t.visibility}${t.listed?`, listed`:``}`)),r(`p`,{class:`muted`},`${t.items.length} files · ${t.shuffle?`shuffle`:`in order`} · ${t.repeat?`repeating`:`once`} · ${t.bitrate} kbps · ${t.listeners} listening`),t.error&&t.state!==`live`?r(`p`,{class:`warn`},t.error):null,...(t.alerts??[]).map(e=>r(`p`,{class:`warn`,role:`status`},`⚠ ${e}`)),t.metrics?r(`p`,{class:`muted`},`This month: ${(t.metrics.month.bytes/1024**2).toFixed(1)} MB delivered · ${Math.round(t.metrics.month.listenerSeconds/3600*10)/10} listener-hours · peak ${t.metrics.month.peakListeners}`):null,E(t),a?r(`p`,{class:`link`},r(`a`,{href:t.links.page,target:`_blank`,rel:`noopener`},`Open the listening page`)):null,r(`div`,{class:`actions`},s,c,o,l,p,u)))}}function O(e,t){g=e;let r=n(`#publish-form`);r.elements.namedItem(`visibility`).value=t?.visibility??e.visibility,r.elements.namedItem(`listed`).checked=t?.listed??(e.visibility!==`public`||e.listed),r.elements.namedItem(`basis`).value=e.rights?.basis??``,r.elements.namedItem(`note`).value=e.rights?.note??``,k(),n(`#publish-dialog`).showModal()}function k(){let e=n(`#publish-form`),t=e.elements.namedItem(`visibility`).value===`public`;n(`#public-only`).hidden=!t,e.elements.namedItem(`basis`).required=t}async function ne(){if(!g)return;let e=n(`#publish-form`),t=e.elements.namedItem(`visibility`).value,r=e.elements.namedItem(`basis`).value,i=e.elements.namedItem(`note`).value,a=await d(`POST`,`/api/v1/stations/${g.id}/access`,{visibility:t,listed:t===`public`&&e.elements.namedItem(`listed`).checked,...r?{rights:{basis:r,note:i}}:{}});t===`private`?await te(g):t===`public`&&e.elements.namedItem(`listed`).checked&&await w(g);let s=t===`private`?`${g.name} is private`:t===`unlisted`?`${g.name} is unlisted: anyone with the link can listen`:`${g.name} is public${e.elements.namedItem(`listed`).checked?` and on the air on live.streaming`:``}`;o(a.dropped?`${s}; ${a.dropped} listeners disconnected`:s,`ok`),await S(),await _(),A(g.id)}function A(e){let t=document.getElementById(`station-${e}`);t&&(t.scrollIntoView({block:`center`}),t.classList.add(`just-changed`),setTimeout(()=>t.classList.remove(`just-changed`),2500))}var j=[],M=new Set,N=[],P=!0;async function F(){let e=l,t=await d(`GET`,`/api/v1/library`);if(e!==l)return;let a=Object.entries(t.counts).filter(([e,t])=>t>0&&e!==`ready`).map(([e,t])=>`${t} ${e}`),o=t.sources.some(e=>e.lastScan?.status===`running`);n(`#library-summary`).textContent=`${t.counts.ready??0} ready to play${a.length?` (${a.join(`, `)})`:``}${o?` · reading new files…`:``}`;let s=n(`#sources`);s.replaceChildren();for(let e of t.sources){let t=r(`button`,{type:`button`,class:`ghost`},`Scan again`);t.addEventListener(`click`,()=>void f(`Scan`,async()=>{await d(`POST`,`/api/v1/library/sources/${String(e.id)}/scan`,{}),i(`Scanning`),setTimeout(()=>void F(),1500)}));let n=e.lastScan;s.append(r(`p`,{class:`source`},r(`code`,{},String(e.root??e.id)),` ${e.enabled?``:`(off) `}`,n?r(`span`,{class:`muted`},`last scan ${String(n.status)}: ${String(n.found)} found, ${String(n.probed)} read`):r(`span`,{class:`muted`},`never scanned`),` `,t))}}var I=0;async function L(e){let t=++I,n=await d(`GET`,`/api/v1/library/items?${new URLSearchParams({q:e,limit:`100`,sort:e?`artist`:`recent`})}`);t===I&&(j=n.items,M.clear(),R(n.total))}function R(e){let t=n(`#results`);if(t.replaceChildren(),j.length===0){t.append(r(`p`,{class:`hint`},`Nothing matches.`)),z();return}let i=r(`table`,{class:`items`},r(`caption`,{class:`sr-only`},`Library items`),r(`thead`,{},r(`tr`,{},r(`th`,{scope:`col`},r(`span`,{class:`sr-only`},`Select`)),r(`th`,{scope:`col`},`Title`),r(`th`,{scope:`col`},`Artist`),r(`th`,{scope:`col`},`Album`),r(`th`,{scope:`col`},`Time`),r(`th`,{scope:`col`},r(`span`,{class:`sr-only`},`Play`))))),a=r(`tbody`);for(let e of j){let t=r(`input`,{type:`checkbox`,"aria-label":`Select ${e.title}`});t.addEventListener(`change`,()=>{t.checked?M.add(e.id):M.delete(e.id),z()});let n=r(`button`,{type:`button`,class:`ghost`,"aria-label":`Play ${e.title}`},`▶`);n.addEventListener(`click`,()=>void q([e.id],0)),a.append(r(`tr`,{},r(`td`,{},t),r(`td`,{title:e.rel},e.title),r(`td`,{},e.artist),r(`td`,{},e.album),r(`td`,{class:`muted`},p(e.duration)),r(`td`,{},n)))}i.append(a),t.append(i),e>j.length&&t.append(r(`p`,{class:`muted`},`Showing ${j.length} of ${e}. Narrow the search to see the rest.`)),z()}function z(){n(`#selection`).hidden=M.size===0,n(`#selection-count`).textContent=`${M.size} selected`,n(`#add-to`).replaceChildren(...N.map(e=>r(`option`,{value:e.id},e.name))),n(`#add-selection`).toggleAttribute(`disabled`,N.length===0),n(`#new-playlist`).disabled=!P}async function B(e){let t=await d(`GET`,`/api/v1/library/browse/${e}?limit=500`),i=n(`#groups`);i.replaceChildren(),i.hidden=!1;for(let n of t.groups){let t=r(`button`,{type:`button`,class:`group`},`${n.name}${n.artist?` — ${n.artist}`:``} (${n.count})`);t.addEventListener(`click`,()=>void f(`Browse`,async()=>{let t={[e===`artists`?`artist`:e===`albums`?`album`:e===`genres`?`genre`:`folder`]:n.name},r=++I,i=await d(`GET`,`/api/v1/library/items?${new URLSearchParams({...t,limit:`500`,sort:`album`})}`);r===I&&(j=i.items,M.clear(),R(i.total))})),i.append(t)}}async function V(){try{N=(await d(`GET`,`/api/v1/library/playlists`)).playlists,P=!0}catch(e){if(e instanceof s&&e.code===`sign_in_required`)P=!1,N=[];else throw e}let e=n(`#playlists`);e.replaceChildren(),P?N.length===0&&e.append(r(`p`,{class:`hint`},`No playlists yet. Select some items above and choose New playlist.`)):e.append(r(`p`,{class:`hint`},`Sign in to nixamp.com (or open this server with its control link) to keep playlists and pick up where you left off on another device.`));for(let t of N){let n=r(`button`,{type:`button`,class:`ghost`},`Open`);n.addEventListener(`click`,()=>void f(`Open playlist`,()=>H(t.id)));let i=r(`button`,{type:`button`},`Play`);i.addEventListener(`click`,()=>void f(`Play`,async()=>{let e=(await d(`GET`,`/api/v1/library/playlists/${t.id}`)).playlist;await q((e.items??[]).filter(e=>e.state===`ready`).map(e=>e.id),0)}));let a=r(`button`,{type:`button`,class:`ghost`},`Publish as a station`);a.addEventListener(`click`,()=>void f(`Publish as a station`,async()=>{let e=await d(`POST`,`/api/v1/stations`,{name:t.name,playlist:t.id,idempotencyKey:`playlist:${t.id}`});await _(),A(e.station.id),o(e.created?`Station ${t.name} made. Choose who can listen to put it on the air.`:`${t.name} already has a station. Choose who can listen.`,`ok`),O(h.find(t=>t.id===e.station.id)??e.station,{visibility:`public`,listed:!0})},a));let s=r(`button`,{type:`button`,class:`ghost danger`},`Delete`);s.addEventListener(`click`,()=>void f(`Delete playlist`,async()=>{confirm(`Delete the playlist "${t.name}"?`)&&(await d(`DELETE`,`/api/v1/library/playlists/${t.id}`),await V())})),e.append(r(`div`,{class:`playlist`,id:`playlist-${t.id}`},r(`strong`,{},t.name),r(`span`,{class:`muted`},` ${t.count} items `),r(`span`,{class:`actions`},i,n,a,s)))}z(),ae()}async function H(e){let t=(await d(`GET`,`/api/v1/library/playlists/${e}`)).playlist,i=n(`#playlist-${e}`);i.querySelector(`ol`)?.remove();let a=t.items??[],o=r(`ol`,{class:`playlist-items`});a.forEach((n,i)=>{let s=n=>async()=>{let r=a.map(e=>e.id),[o]=r.splice(i,1);r.splice(Math.max(0,Math.min(r.length,i+n)),0,o),await d(`PATCH`,`/api/v1/library/playlists/${e}`,{items:r,revision:t.revision}),await H(e)},c=r(`button`,{type:`button`,class:`ghost`,disabled:i===0,"aria-label":`Move ${n.title??`item`} up`},`↑`);c.addEventListener(`click`,()=>void f(`Move`,s(-1)));let l=r(`button`,{type:`button`,class:`ghost`,disabled:i===a.length-1,"aria-label":`Move ${n.title??`item`} down`},`↓`);l.addEventListener(`click`,()=>void f(`Move`,s(1)));let u=r(`button`,{type:`button`,class:`ghost`,"aria-label":`Remove ${n.title??`item`}`},`✕`);u.addEventListener(`click`,()=>void f(`Remove`,async()=>{await d(`PATCH`,`/api/v1/library/playlists/${e}`,{items:a.filter((e,t)=>t!==i).map(e=>e.id),revision:t.revision}),await H(e),await V()})),o.append(r(`li`,{},`${n.title??n.id}${n.artist?` — ${n.artist}`:``}`,n.state===`ready`?null:r(`span`,{class:`warn`},` (${n.state})`),` `,c,l,u))}),i.append(o)}var U=[],W=0,G=0,K=/Mobi|Android|iPhone/i.test(navigator.userAgent)?`phone`:`computer`;async function q(e,t,n=0){e.length!==0&&(U=e,W=t,P&&await d(`PUT`,`/api/v1/library/queue`,{items:e,device:K}).catch(()=>void 0),await X(n))}var J=null;function Y(){J?.destroy(),J=null}async function X(t=0){let r=U[W];if(!r)return;let i=n(`#audio`);Y(),J=await e(i,{src:u(`/api/v1/library/items/${r}/stream`,!0),kind:`audio`}),t>0&&(i.currentTime=t);let a=j.find(e=>e.id===r)??await d(`GET`,`/api/v1/library/items/${r}`).then(e=>e.item).catch(()=>null);n(`#now`).textContent=a?`${a.title}${a.artist?` — ${a.artist}`:``}`:``,await i.play().catch(()=>void 0),P&&await d(`PATCH`,`/api/v1/library/queue`,{at:W,position:t,device:K}).catch(()=>void 0)}function re(){let e=n(`#audio`);e.addEventListener(`ended`,()=>{W+1<U.length&&(W+=1,X())}),e.addEventListener(`timeupdate`,()=>{!P||Date.now()-G<1e4||(G=Date.now(),d(`PATCH`,`/api/v1/library/queue`,{at:W,position:e.currentTime,device:K}).catch(()=>void 0))})}async function ie(){if(!P)return;let e=(await d(`GET`,`/api/v1/library/queue`).catch(()=>null))?.queue,t=e?.items[e.at];if(!e||!t||e.items.length===0)return;let i=n(`#resume`),a=r(`button`,{type:`button`},`Resume`);a.addEventListener(`click`,()=>void f(`Resume`,async()=>{U=e.items.map(e=>e.id),W=e.at,await X(e.position),i.hidden=!0})),i.replaceChildren(r(`span`,{},`Where you left off${e.device?` on your ${e.device}`:``}: ${t.title??``} at ${p(e.position)||`the start`}. `),a),i.hidden=!1}async function ae(){let e=n(`#station-from`),t=await d(`GET`,`/api/v1/library/browse/folders?limit=500`).catch(()=>({groups:[]}));e.replaceChildren(r(`option`,{value:`folder:`},`The whole library`),...N.length?[r(`optgroup`,{label:`Playlists`},...N.map(e=>r(`option`,{value:`playlist:${e.id}`},`${e.name} (${e.count})`)))]:[],r(`optgroup`,{label:`Folders`},...t.groups.map(e=>r(`option`,{value:`folder:${e.name}`},`${e.name} (${e.count})`))))}async function oe(e){let t=new FormData(e),[r,i]=String(t.get(`from`)??`folder:`).split(/:(.*)/s),a=await d(`POST`,`/api/v1/stations`,{name:String(t.get(`name`)??``),...r===`playlist`?{playlist:i}:{folder:i??``},shuffle:t.get(`shuffle`)===`on`,repeat:t.get(`repeat`)===`on`,bitrate:Number(t.get(`bitrate`)??128)});e.reset(),n(`#new-station`).open=!1,await _(),A(a.station.id),o(`Station ${a.station.name} made; it is private and stopped. Use "Who can listen" to put it on the air.`,`ok`)}async function se(){re(),n(`#search-form`).addEventListener(`submit`,e=>{e.preventDefault();let t=e.currentTarget,r=t.elements.namedItem(`browse`).value,i=t.elements.namedItem(`q`).value.trim();n(`#groups`).hidden=!0,f(`Search`,()=>r&&!i?B(r):L(i))}),n(`#search-form select`).addEventListener(`change`,e=>{let t=e.currentTarget.value;t&&f(`Browse`,()=>B(t))}),n(`#play-selection`).addEventListener(`click`,()=>void f(`Play`,()=>q(j.filter(e=>M.has(e.id)).map(e=>e.id),0))),n(`#new-playlist`).addEventListener(`click`,()=>void f(`New playlist`,async()=>{let e=prompt(`Name the playlist`);e&&(await d(`POST`,`/api/v1/library/playlists`,{name:e,items:j.filter(e=>M.has(e.id)).map(e=>e.id)}),i(`Playlist ${e} made`),await V())})),n(`#add-selection`).addEventListener(`click`,()=>void f(`Add to playlist`,async()=>{let e=n(`#add-to`).value;await d(`POST`,`/api/v1/library/playlists/${e}/items`,{items:j.filter(e=>M.has(e.id)).map(e=>e.id)}),i(`Added`),await V()})),n(`#station-form`).addEventListener(`submit`,e=>{e.preventDefault();let t=e.currentTarget;f(`Make station`,()=>oe(t),t.querySelector(`button[type=submit]`)??void 0)}),n(`#source-form`).addEventListener(`submit`,e=>{e.preventDefault();let t=e.currentTarget,n=t.elements.namedItem(`root`).value.trim();f(`Add source`,async()=>{await d(`POST`,`/api/v1/library/sources`,{root:n}),t.reset(),i(`Added; reading its files`),setTimeout(()=>void F(),1500)})}),n(`#publish-form`).addEventListener(`change`,k),n(`#publish-dialog`).addEventListener(`close`,()=>{n(`#publish-dialog`).returnValue===`save`&&f(`Who can listen`,ne)});let e=await ce(),t=n(`#server`);t.replaceChildren(...e.map(e=>r(`option`,{value:e.id},e.name))),e.length===0&&t.append(r(`option`,{value:``},`No servers`));let a=(()=>{try{return localStorage.getItem(Z)??``}catch{return``}})(),s=e.find(e=>e.id===a)??e[0];if(t.addEventListener(`change`,()=>{let n=e.find(e=>e.id===t.value);n&&$(n)}),setInterval(()=>void _().catch(()=>void 0),5e3),setInterval(()=>void F().catch(()=>void 0),15e3),setInterval(()=>void S().then(D).catch(()=>void 0),15e3),!s){o(Q()?`Sign in to nixamp.com to see your servers here. A server shows up once it has run signed in to your account (nixamp login on that machine).`:`This page manages the server it is on.`);return}t.value=s.id,await $(s),location.hash===`#stations`&&document.querySelector(`#stations`)?.scrollIntoView()}var Z=`nixamp.radio.server`;function Q(){return/^(www\.)?nixamp\.com$/i.test(location.hostname)}async function ce(){let e=Q()?[]:[{id:`here`,name:`This server (${location.host})`,base:``,session:``,key:``}],n=``;try{let e=await fetch(`/api/v1/auth/token`,{credentials:`same-origin`}),t=await e.json().catch(()=>({}));n=e.ok&&typeof t.token==`string`?t.token:``}catch{n=``}if(!n)return e;try{y=(await fetch(`/api/v1/events?mine=true&kind=class&status=live&limit=1`,{credentials:`same-origin`})).ok}catch{y=!1}try{let r=await(await fetch(`/api/v1/servers`,{credentials:`same-origin`})).json().catch(()=>({}));for(let i of r.servers??[]){let{base:r,key:a}=t(i.url);/^https?:\/\//.test(r)&&r.replace(/\/+$/,``)!==location.origin&&(location.protocol===`https:`&&r.startsWith(`http://`)||e.find(e=>e.base===r.replace(/\/+$/,``))||e.push({id:i.id,name:i.name||new URL(r).host,base:r.replace(/\/+$/,``),session:n,key:i.key||a}))}}catch{}return e}async function $(e){l+=1,I+=1,c=e;try{localStorage.setItem(Z,e.id)}catch{}h=[],j=[],M.clear(),U=[];let t=n(`#audio`);t.pause(),Y(),t.removeAttribute(`src`),n(`#now`).textContent=``,n(`#resume`).hidden=!0,n(`#groups`).hidden=!0,D(),o(``);try{await S().catch(()=>void 0),await le(e)}catch(t){t instanceof s&&(t.status===401||t.status===403)?o(e.base?`${e.name} did not accept your nixamp.com sign-in. Is it running signed in as you? (on that machine: nixamp login, then nixamp daemon restart)`:`This page manages the server it is on. Open it from your server's admin link (nixamp admin prints it), or sign in as its owner.`):t instanceof s&&t.status===404?o(`${e.name} has no library or stations: it needs nixamp 0.32 or later (nixamp update on that machine).`):t instanceof TypeError?o(`Could not reach ${e.name} at ${e.base||location.origin}. Is it on?`):o(t instanceof Error?t.message:String(t));return}await ie()}async function le(e){let t=l;try{await Promise.all([F(),_(),V(),L(``)])}catch(n){if(!(n instanceof s&&(n.status===401||n.status===403)&&e.session&&e.key)||t!==l)throw n;c={...e,session:``},await Promise.all([F(),_(),V(),L(``)])}}se();