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:
1
server/public/assets/admin-BRU0y9A4.js
Normal file
1
server/public/assets/admin-BRU0y9A4.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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))}
|
||||
1
server/public/assets/docs-z3ZiwvpP.css
Normal file
1
server/public/assets/docs-z3ZiwvpP.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--bg-0: #07080b;--bg-1: #0e1116;--bg-2: #161a22;--bg-3: #1f242e;--line: #262b36;--fg: #e9ecf2;--muted: #8a90a0;--muted-2: #5d6373;--accent: #ff7a3d;--accent-2: #ffb37a;--good: #4ec9a6;--bad: #ec6a6a;--info: #6ab7ff;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color-scheme:dark}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg-0);color:var(--fg)}a{color:var(--accent-2);text-decoration:none}a:hover{text-decoration:underline}.docs-header{position:sticky;top:0;z-index:10;background:#07080beb;border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.docs-header-inner{max-width:980px;margin:0 auto;padding:14px 20px;display:flex;align-items:center;gap:16px}.docs-header h1{margin:0;font-size:18px;letter-spacing:-.01em}.docs-header .back{color:var(--muted);font-size:13px;padding:6px 10px;border:1px solid var(--line);border-radius:8px}.docs-header .back:hover{color:var(--fg);background:var(--bg-2);text-decoration:none}.docs-header .base{margin-left:auto;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--muted);padding:4px 10px;background:var(--bg-2);border:1px solid var(--line);border-radius:8px}#app{max-width:980px;margin:0 auto;padding:24px 20px 80px}h2.group{margin:32px 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted)}h2.group:first-child{margin-top:0}.ep{background:var(--bg-1);border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:14px}.ep-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ep-path{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px;color:var(--fg);background:var(--bg-2);padding:4px 10px;border-radius:6px;border:1px solid var(--line);overflow-wrap:anywhere}.ep-sum{margin:10px 0 0;color:var(--muted);font-size:14px;line-height:1.5}.m{display:inline-block;font-size:11px;font-weight:800;letter-spacing:.06em;padding:4px 8px;border-radius:6px;color:#07080b}.m-get{background:var(--good)}.m-post{background:var(--accent)}.m-put{background:#d9b14a}.m-delete{background:var(--bad);color:#fff}.m-info{background:var(--info)}.params{width:100%;border-collapse:collapse;margin-top:14px;font-size:13px}.params th{text-align:left;font-weight:600;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:.06em;padding:8px 10px;border-bottom:1px solid var(--line)}.params td{padding:8px 10px;border-bottom:1px solid var(--line);color:var(--fg)}.params td:first-child{width:160px}.params code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--accent-2);background:var(--bg-2);padding:2px 6px;border-radius:4px}.examples{margin-top:14px}.examples-h{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}.examples pre{margin:0 0 8px;padding:10px 12px;background:var(--bg-0);border:1px solid var(--line);border-radius:8px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12.5px;color:var(--accent-2);overflow-x:auto}.try{margin-top:14px}.try-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.try-q{flex:1;background:var(--bg-2);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;font-family:ui-monospace,SF Mono,Menlo,monospace;outline:none}.try-q:focus{border-color:var(--accent)}.try-btn{background:var(--accent);color:#1a0a00;border:0;border-radius:8px;padding:8px 16px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit}.try-btn:hover{background:#ff8a55}.try-btn:disabled{opacity:.6;cursor:default}.try-open{color:var(--muted);font-size:12px;padding:6px 10px;border:1px solid var(--line);border-radius:8px}.try-open:hover{color:var(--fg);background:var(--bg-2);text-decoration:none}.try-out{margin:0;padding:12px;background:var(--bg-0);border:1px solid var(--line);border-radius:8px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--fg);max-height:320px;overflow:auto;white-space:pre-wrap;word-break:break-word}
|
||||
@@ -1 +0,0 @@
|
||||
(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))s(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&s(c)}).observe(document,{childList:!0,subtree:!0});function o(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function s(e){if(e.ep)return;e.ep=!0;const t=o(e);fetch(e.href,t)}})();async function i(r,n,o){const s=await fetch(n,{method:r,credentials:"same-origin",headers:o?{"Content-Type":"application/json"}:{},body:o?JSON.stringify(o):void 0});if(s.status===204)return null;const t=(s.headers.get("content-type")||"").includes("json")?await s.json():await s.text();if(!s.ok)throw Object.assign(new Error((t==null?void 0:t.error)||s.statusText),{status:s.status,data:t});return t}const l={get:r=>i("GET",r),post:(r,n)=>i("POST",r,n),put:(r,n)=>i("PUT",r,n),patch:(r,n)=>i("PATCH",r,n),del:r=>i("DELETE",r)};function a(r,n={},...o){const s=document.createElement(r);for(const[e,t]of Object.entries(n||{}))e==="class"?s.className=t:e==="style"&&typeof t=="object"?Object.assign(s.style,t):e.startsWith("on")&&typeof t=="function"?s.addEventListener(e.slice(2).toLowerCase(),t):e==="html"?s.innerHTML=t:t!==!1&&t!=null&&s.setAttribute(e,t===!0?"":t);for(const e of o.flat())e==null||e===!1||s.appendChild(e instanceof Node?e:document.createTextNode(String(e)));return s}function f(r){for(;r.firstChild;)r.removeChild(r.firstChild)}export{l as a,f as c,a as e};
|
||||
1
server/public/assets/dom-BvorgAdo.js
Normal file
1
server/public/assets/dom-BvorgAdo.js
Normal file
@@ -0,0 +1 @@
|
||||
async function a(t,i,o){const s=await fetch(i,{method:t,credentials:"same-origin",headers:o?{"Content-Type":"application/json"}:{},body:o?JSON.stringify(o):void 0});if(s.status===204)return null;const e=(s.headers.get("content-type")||"").includes("json")?await s.json():await s.text();if(!s.ok)throw Object.assign(new Error((e==null?void 0:e.error)||s.statusText),{status:s.status,data:e});return e}const c={get:t=>a("GET",t),post:(t,i)=>a("POST",t,i),put:(t,i)=>a("PUT",t,i),patch:(t,i)=>a("PATCH",t,i),del:t=>a("DELETE",t)};function r(t,i={},...o){const s=document.createElement(t);for(const[n,e]of Object.entries(i||{}))n==="class"?s.className=e:n==="style"&&typeof e=="object"?Object.assign(s.style,e):n.startsWith("on")&&typeof e=="function"?s.addEventListener(n.slice(2).toLowerCase(),e):n==="html"?s.innerHTML=e:e!==!1&&e!=null&&s.setAttribute(n,e===!0?"":e);for(const n of o.flat())n==null||n===!1||s.appendChild(n instanceof Node?n:document.createTextNode(String(n)));return s}function l(t){for(;t.firstChild;)t.removeChild(t.firstChild)}export{c as a,l as c,r as e};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
server/public/assets/kiosk-CdZttV5P.css
Normal file
1
server/public/assets/kiosk-CdZttV5P.css
Normal file
File diff suppressed because one or more lines are too long
1
server/public/assets/modulepreload-polyfill-B5Qt9EMX.js
Normal file
1
server/public/assets/modulepreload-polyfill-B5Qt9EMX.js
Normal file
@@ -0,0 +1 @@
|
||||
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
|
||||
Reference in New Issue
Block a user