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+