commit fcfda12
uint
·
2026-01-26 21:59:24 +0000 UTC
parent 243ab69
add basic webui .
3 files changed,
+311,
-0
+39,
-0
1@@ -0,0 +1,39 @@
2+<!doctype html>
3+<html>
4+ <head>
5+ <meta charset="utf-8">
6+ <meta name="viewport" content="width=device-width, initial-scale=1">
7+ <title>parados</title>
8+ <link rel="stylesheet" href="style.css">
9+ </head>
10+ <body>
11+ <header>
12+ <h1>parados</h1>
13+
14+ <form id="cfg" autocomplete="off">
15+ <input id="base" type="text" value="http://127.0.0.1:8088">
16+ <button id="load" type="submit">load</button>
17+ <span id="status"></span>
18+ </form>
19+ </header>
20+
21+ <main>
22+ <section id="left">
23+ <input id="filter" type="text" placeholder="filter...">
24+ <ul id="list"></ul>
25+ </section>
26+
27+ <section id="right">
28+ <div id="now"></div>
29+
30+ <audio id="aud" controls preload="metadata"></audio>
31+ <video id="vid" controls preload="metadata"></video>
32+
33+ <pre id="meta"></pre>
34+ </section>
35+ </main>
36+
37+ <script src="parados.js"></script>
38+ </body>
39+</html>
40+
+258,
-0
1@@ -0,0 +1,258 @@
2+"use strict";
3+
4+let items = [];
5+let active_id = null;
6+
7+function $(id)
8+{
9+ return document.getElementById(id);
10+}
11+
12+function status_set(s)
13+{
14+ const e = $("status");
15+ if (e)
16+ e.textContent = s;
17+}
18+
19+function base_url()
20+{
21+ const e = $("base");
22+ const s = e ? (e.value || "") : "";
23+ return s.replace(/\/+$/, "");
24+}
25+
26+function human_size(n)
27+{
28+ if (typeof n !== "number" || !isFinite(n))
29+ return "" + n;
30+
31+ const u = ["B","KB","MB","GB","TB"];
32+ let i = 0;
33+ while (n >= 1024 && i < u.length - 1) {
34+ n /= 1024;
35+ i++;
36+ }
37+ return (i === 0 ? n.toFixed(0) : n.toFixed(1)) + " " + u[i];
38+}
39+
40+function is_video(type, path)
41+{
42+ if (type && type.startsWith("video/"))
43+ return true;
44+
45+ return /\.(mp4|mkv|webm|mov)$/i.test(path || "");
46+}
47+
48+function is_audio(type, path)
49+{
50+ if (type && type.startsWith("audio/"))
51+ return true;
52+
53+ return /\.(mp3|m4a|aac|flac|wav|ogg|opus)$/i.test(path || "");
54+}
55+
56+function clear_players()
57+{
58+ const a = $("aud");
59+ const v = $("vid");
60+
61+ if (a) {
62+ a.pause();
63+ a.removeAttribute("src");
64+ a.style.display = "none";
65+ }
66+ if (v) {
67+ v.pause();
68+ v.removeAttribute("src");
69+ v.style.display = "none";
70+ }
71+}
72+
73+async function api_json(url)
74+{
75+ const r = await fetch(url, { method: "GET" });
76+ if (!r.ok) {
77+ let t = "";
78+ try {
79+ t = await r.text();
80+ }
81+ catch (e) {
82+ t = "";
83+ }
84+ t = (t || "").trim();
85+ throw new Error("http " + r.status + (t ? (": " + t) : ""));
86+ }
87+
88+ return await r.json();
89+}
90+
91+function render_list()
92+{
93+ const list = $("list");
94+ const f = $("filter");
95+
96+ if (!list)
97+ return;
98+
99+ const q = (f ? (f.value || "") : "").toLowerCase();
100+
101+ list.innerHTML = "";
102+ for (const it of items) {
103+ if (q && !(it.path || "").toLowerCase().includes(q))
104+ continue;
105+
106+ const li = document.createElement("li");
107+ li.dataset.id = it.id;
108+ li.textContent = it.path || it.id;
109+
110+ if (it.id === active_id)
111+ li.classList.add("active");
112+
113+ li.addEventListener("click", () => select_item(it.id));
114+
115+ list.appendChild(li);
116+ }
117+}
118+
119+function set_active_class()
120+{
121+ const list = $("list");
122+ if (!list)
123+ return;
124+
125+ const lis = list.querySelectorAll("li");
126+ for (const li of lis) {
127+ if (li.dataset.id === active_id)
128+ li.classList.add("active");
129+ else
130+ li.classList.remove("active");
131+ }
132+}
133+
134+async function load_library()
135+{
136+ status_set("loading...");
137+ clear_players();
138+
139+ const now = $("now");
140+ const meta = $("meta");
141+
142+ if (now)
143+ now.textContent = "";
144+ if (meta)
145+ meta.textContent = "";
146+
147+ const base = base_url();
148+ const j = await api_json(base + "/library");
149+
150+ if (!j || !Array.isArray(j.items))
151+ throw new Error("bad /library json");
152+
153+ items = j.items.slice();
154+ active_id = null;
155+
156+ status_set("ok (" + items.length + " items)");
157+ render_list();
158+}
159+
160+async function select_item(id)
161+{
162+ active_id = id;
163+ set_active_class();
164+ clear_players();
165+
166+ const base = base_url();
167+
168+ const it = items.find(x => x.id === id);
169+ const path = it ? (it.path || "") : "";
170+
171+ const now = $("now");
172+ const meta = $("meta");
173+ const a = $("aud");
174+ const v = $("vid");
175+
176+ if (now)
177+ now.textContent = path ? path : ("id " + id);
178+ if (meta)
179+ meta.textContent = "loading meta...";
180+
181+ let m = null;
182+ try {
183+ m = await api_json(base + "/meta/" + id);
184+ }
185+ catch (e) {
186+ if (meta)
187+ meta.textContent = "" + e;
188+ return;
189+ }
190+
191+ const type = m.type || "";
192+ const size = Number(m.size);
193+
194+ if (meta) {
195+ meta.textContent =
196+ "id: " + (m.id || id) + "\n" +
197+ "path: " + (m.path || path) + "\n" +
198+ "type: " + type + "\n" +
199+ "size: " + (isFinite(size) ? human_size(size) : (m.size || "")) + "\n";
200+ }
201+
202+ const stream = base + "/stream/" + id;
203+
204+ if (v && is_video(type, m.path || path)) {
205+ v.src = stream;
206+ v.style.display = "block";
207+ v.load();
208+ return;
209+ }
210+
211+ if (a && is_audio(type, m.path || path)) {
212+ a.src = stream;
213+ a.style.display = "block";
214+ a.load();
215+ return;
216+ }
217+
218+ if (meta)
219+ meta.textContent += "\n(no player for this type)\n";
220+}
221+
222+async function main()
223+{
224+ const cfg = $("cfg");
225+ const load = $("load");
226+ const filter = $("filter");
227+
228+ if (cfg) {
229+ cfg.addEventListener("submit", async (ev) => {
230+ ev.preventDefault();
231+ if (load)
232+ load.disabled = true;
233+
234+ try {
235+ await load_library();
236+ }
237+ catch (e) {
238+ status_set("error: " + (e.message || e));
239+ }
240+ finally {
241+ if (load)
242+ load.disabled = false;
243+ }
244+ });
245+ }
246+
247+ if (filter)
248+ filter.addEventListener("input", render_list);
249+
250+ try {
251+ await load_library();
252+ }
253+ catch (e) {
254+ status_set("error: " + (e.message || e));
255+ }
256+}
257+
258+main();
259+
+14,
-0
1@@ -0,0 +1,14 @@
2+body {
3+ color: #eeeeee;
4+ background-color: #1e1f21;
5+ font-family: serif;
6+ margin: 20px;
7+}
8+
9+video {
10+ width: 100%;
11+ max-width: 400px;
12+ background: #000;
13+ display: block;
14+}
15+