Add API documentation and underground station importer
- Introduced a new HTML documentation page for the oradio API, including a JavaScript file to handle dynamic content and API requests. - Added a CSS file for styling the documentation page. - Implemented an underground station importer script that fetches data from Radio-Browser and writes it to a JSON file. - Created a stats module to compute and manage vote and play statistics for radio stations. - Added a polyfill for modulepreload to ensure compatibility with older browsers.
This commit is contained in:
5
server/public/assets/docs-CJfnRuXm.js
Normal file
5
server/public/assets/docs-CJfnRuXm.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import"./modulepreload-polyfill-B5Qt9EMX.js";const p=`${location.origin}/api/v1`,u=`${location.origin}/api`;document.getElementById("base").textContent=p;const C=[{group:"Public (v1)",items:[{id:"health",method:"GET",path:"/health",summary:"Service heartbeat plus enabled-station count.",tryable:!0},{id:"categories",method:"GET",path:"/categories",summary:"All categories with their station counts.",tryable:!0},{id:"stations-list",method:"GET",path:"/stations",summary:"Paginated station list. Filterable and sortable.",params:[{name:"q",desc:"Substring filter on name / genres / country."},{name:"category",desc:"Category id (see /categories)."},{name:"country",desc:"ISO country code, case-insensitive."},{name:"genre",desc:"Substring match against any genre."},{name:"sort",desc:"hot | top | plays | controversial | name (default: name)."},{name:"limit",desc:"Max items returned (default 200, cap 1000)."}],tryable:!0,tryQuery:"limit=3&sort=hot"},{id:"random",method:"GET",path:"/stations/random",summary:"Pick one random enabled station. Same filters as /stations. Pass redirect=stream for a 302 to the resolved audio URL.",params:[{name:"category",desc:"Restrict pool to a category."},{name:"country",desc:"Restrict pool to a country."},{name:"genre",desc:"Restrict pool by genre substring."},{name:"redirect",desc:'Set to "stream" to 302-redirect to the resolved stream URL.'}],tryable:!0,examples:[`mpv ${p}/stations/random?redirect=stream`,`curl -sLI "${p}/stations/random?redirect=stream" | grep -i location`]},{id:"station",method:"GET",path:"/stations/{uuid}",summary:"Full detail for one station, including its streams.",params:[{name:"uuid",desc:"Station UUID (see list response)."}]},{id:"station-stream",method:"GET",path:"/stations/{uuid}/stream",summary:"302-redirect to the resolved stream URL. Picks the highest-priority stream that was last seen up.",params:[{name:"uuid",desc:"Station UUID."},{name:"format",desc:"Optional preferred format (mp3, aac, ogg, hls)."}]},{id:"stream-by-uuid",method:"GET",path:"/stations/{uuid}/streams/{streamUuid}",summary:"Resolve and 302 to a specific stream. Pass redirect=0 to return JSON metadata instead.",params:[{name:"uuid",desc:"Station UUID."},{name:"streamUuid",desc:"Stream UUID."},{name:"redirect",desc:'Set to "0" to return JSON instead of redirecting.'}]}]},{group:"Authenticated (cookie session)",items:[{id:"me",method:"GET",path:"/auth/me",base:u,summary:"Current signed-in user, or 401.",tryable:!0},{id:"favorites",method:"GET",path:"/me/favorites",base:u,summary:"Your favorites, ordered.",tryable:!0},{id:"favorites-random",method:"GET",path:"/me/favorites/random",base:u,summary:'One random favorite — used by the kiosk dice button in "favorites" mode.',tryable:!0},{id:"history",method:"GET",path:"/me/history",base:u,summary:"Recent play history (last 50).",tryable:!0}]},{group:"Rate limit",items:[{id:"rate",method:"INFO",path:"120 req / minute / IP",summary:"Public /api/v1 endpoints share a per-IP token bucket. Headers X-RateLimit-Limit and X-RateLimit-Remaining tell you where you stand."}]}],f=document.getElementById("app");function e(t,i,...d){const s=document.createElement(t);if(i)for(const[a,o]of Object.entries(i))o==null||o===!1||(a==="class"?s.className=o:a.startsWith("on")&&typeof o=="function"?s.addEventListener(a.slice(2).toLowerCase(),o):s.setAttribute(a,o));for(const a of d)a==null||a===!1||s.appendChild(typeof a=="string"?document.createTextNode(a):a);return s}function E(t){return e("span",{class:`m m-${t.toLowerCase()}`},t)}function T(t){var a,o;const d=`${t.base||p}${t.path}`,s=e("article",{class:"ep",id:t.id});if(s.appendChild(e("header",{class:"ep-head"},E(t.method),e("code",{class:"ep-path"},d))),s.appendChild(e("p",{class:"ep-sum"},t.summary)),(a=t.params)!=null&&a.length){const n=e("table",{class:"params"},e("thead",{},e("tr",{},e("th",{},"Parameter"),e("th",{},"Description"))),e("tbody",{},...t.params.map(m=>e("tr",{},e("td",{},e("code",{},m.name)),e("td",{},m.desc)))));s.appendChild(n)}if((o=t.examples)!=null&&o.length&&s.appendChild(e("div",{class:"examples"},e("div",{class:"examples-h"},"Examples"),...t.examples.map(n=>e("pre",{},e("code",{},n))))),t.tryable&&t.method==="GET"){const n=e("pre",{class:"try-out"},'Click "Try it" to send a live request.'),m=e("input",{class:"try-q",type:"text",placeholder:"?key=value (optional)",value:t.tryQuery?`?${t.tryQuery}`:""}),c=e("button",{class:"try-btn",onClick:async()=>{c.disabled=!0,c.textContent="…";let l=m.value.trim();l&&!l.startsWith("?")&&(l=`?${l}`);const h=`${d}${l}`,g=performance.now();try{const r=await fetch(h,{credentials:"same-origin",redirect:"manual"}),v=Math.round(performance.now()-g);let y;r.type==="opaqueredirect"||r.status>=300&&r.status<400?y="(redirect — open in new tab to follow)":y=(r.headers.get("content-type")||"").includes("json")?JSON.stringify(await r.json(),null,2):(await r.text()).slice(0,4e3),n.textContent=`${r.status} ${r.statusText||""} · ${v} ms
|
||||
${h}
|
||||
|
||||
${y}`}catch(r){n.textContent=`error: ${r.message||r}
|
||||
${h}`}finally{c.disabled=!1,c.textContent="Try it"}}},"Try it"),b=e("a",{class:"try-open",target:"_blank",rel:"noopener",href:d},"Open ↗");s.appendChild(e("div",{class:"try"},e("div",{class:"try-row"},m,c,b),n))}return s}for(const t of C){f.appendChild(e("h2",{class:"group"},t.group));for(const i of t.items)f.appendChild(T(i))}
|
||||
Reference in New Issue
Block a user