commit bc3c3d0

uint  ·  2026-02-09 19:36:10 +0000 UTC
parent c3951e1
gorados: complete (for now)
4 files changed,  +186, -85
+164, -62
  1@@ -1,6 +1,6 @@
  2 // gorados
  3-// is a Go frontend for the parados
  4-// HTTP media server
  5+// is a Go-Web frontendfor the
  6+// parados HTTP media server
  7 
  8 package main
  9 
 10@@ -15,18 +15,18 @@ import (
 11 	"time"
 12 )
 13 
 14+// structs
 15+
 16 // library item
 17 type item struct {
 18 	ID   string `json:"id"`
 19 	Path string `json:"path"`
 20 }
 21-
 22 // upstream response
 23 type library_resp struct {
 24 	Proto int    `json:"proto"`
 25 	Items []item `json:"items"`
 26 }
 27-
 28 // /meta/{id} response
 29 type meta_resp struct {
 30 	Proto int    `json:"proto"`
 31@@ -38,20 +38,67 @@ type meta_resp struct {
 32 	Type  string `json:"type"`
 33 	Kind  string `json:"kind"`
 34 }
 35-
 36 // app state
 37 type handler struct {
 38-	parados string
 39-	user    string
 40-	pass    string
 41-	httpc   *http.Client
 42+	parados  string
 43+	web_dir  string
 44+	httpc    *http.Client
 45+}
 46+
 47+// functions
 48+func (h *handler) serve_file(w http.ResponseWriter, r *http.Request, path string, ctype string) {
 49+	p := h.web_dir + "/" + path
 50+
 51+	b, err := os.ReadFile(p)
 52+	if err != nil {
 53+		http.Error(w, "Web Error: "+p+": "+err.Error(), 500)
 54+		return
 55+	}
 56+
 57+	if ctype != "" {
 58+		w.Header().Set("Content-Type", ctype)
 59+	}
 60+
 61+	w.WriteHeader(200)
 62+	w.Write(b)
 63+}
 64+
 65+func (h *handler) load_web(path string) (string, error) {
 66+	p := h.web_dir + "/" + path
 67+
 68+	b, err := os.ReadFile(p)
 69+	if err != nil {
 70+		return "", err
 71+	}
 72+
 73+	return string(b), nil
 74+}
 75+
 76+func (h *handler) web_subst(w http.ResponseWriter, body string, kv map[string]string) {
 77+	for k, v := range kv {
 78+		body = strings.ReplaceAll(body, k, v)
 79+	}
 80+
 81+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 82+	w.WriteHeader(200)
 83+	w.Write([]byte(body))
 84 }
 85 
 86-func (h *handler) get_json(url string, out any) (int, error) {
 87-	req, _ := http.NewRequest("GET", url, nil)
 88-	if h.user != "" {
 89-		req.SetBasicAuth(h.user, h.pass)
 90+func (h *handler) pass_auth(dst *http.Request, src *http.Request) {
 91+	a := src.Header.Get("Authorization")
 92+	if a != "" {
 93+		dst.Header.Set("Authorization", a)
 94 	}
 95+}
 96+
 97+func (h *handler) upstream_req(method string, path string, r *http.Request) *http.Request {
 98+	req, _ := http.NewRequest(method, h.parados+path, nil)
 99+	h.pass_auth(req, r)
100+	return req
101+}
102+
103+func (h *handler) upstream_json(path string, r *http.Request, out any) (int, error) {
104+	req := h.upstream_req("GET", path, r)
105 
106 	resp, err := h.httpc.Do(req)
107 	if err != nil {
108@@ -66,30 +113,77 @@ func (h *handler) get_json(url string, out any) (int, error) {
109 	return 200, json.NewDecoder(resp.Body).Decode(out)
110 }
111 
112+func (h *handler) index(w http.ResponseWriter, r *http.Request) {
113+	if r.URL.Path != "/" && r.URL.Path != "/index.html" {
114+		http.NotFound(w, r)
115+		return
116+	}
117+
118+	h.serve_file(w, r, "index.html", "text/html; charset=utf-8")
119+}
120+
121+func (h *handler) style(w http.ResponseWriter, r *http.Request) {
122+	h.serve_file(w, r, "style.css", "text/css; charset=utf-8")
123+}
124+
125+func (h *handler) ping(w http.ResponseWriter, r *http.Request) {
126+	req := h.upstream_req("GET", "/ping", r)
127+
128+	resp, err := h.httpc.Do(req)
129+	if err != nil {
130+		http.Error(w, "Upstream Error", 502)
131+		return
132+	}
133+	defer resp.Body.Close()
134+
135+	w.WriteHeader(resp.StatusCode)
136+
137+	if r.Method == "HEAD" {
138+		return
139+	}
140+
141+	io.Copy(w, resp.Body)
142+}
143+
144 func (h *handler) library(w http.ResponseWriter, r *http.Request) {
145 	var lr library_resp
146-	code, err := h.get_json(h.parados+"/library", &lr)
147+	code, err := h.upstream_json("/library", r, &lr)
148 	if err != nil {
149 		http.Error(w, "Upstream Error", 502)
150 		return
151 	}
152+
153+	if code == 401 {
154+		w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
155+		http.Error(w, "Unauthorized", 401)
156+		return
157+	}
158+
159 	if code != 200 {
160 		http.Error(w, http.StatusText(code), code)
161 		return
162 	}
163+
164 	if lr.Proto != 1 {
165 		http.Error(w, "Unknown Proto Version", 502)
166 		return
167 	}
168 
169-	// tmp html
170-	w.Header().Set("Content-Type", "text/html; charset=utf-8")
171-	fmt.Fprintf(w, "<!doctype html><meta charset=utf-8><title>parados</title>")
172-	fmt.Fprintf(w, "<h1>parados</h1><p>items: %d</p><ul>", len(lr.Items))
173+	body, err := h.load_web("library.html")
174+	if err != nil {
175+		http.Error(w, "Web Error: "+h.web_dir+"/library.html: "+err.Error(), 500)
176+		return
177+	}
178+
179+	var items strings.Builder
180 	for _, it := range lr.Items {
181-		fmt.Fprintf(w, `<li><a href="/item/%s">%s</a></li>`, it.ID, it.Path)
182+		fmt.Fprintf(&items, `<li><a href="/item/%s">%s</a></li>`+"\n", it.ID, it.Path)
183 	}
184-	fmt.Fprintf(w, "</ul>")
185+
186+	h.web_subst(w, body, map[string]string{
187+		"{{COUNT}}": fmt.Sprintf("%d", len(lr.Items)),
188+		"{{ITEMS}}": items.String(),
189+	})
190 }
191 
192 func (h *handler) item(w http.ResponseWriter, r *http.Request) {
193@@ -100,47 +194,62 @@ func (h *handler) item(w http.ResponseWriter, r *http.Request) {
194 	}
195 
196 	var mr meta_resp
197-	code, err := h.get_json(h.parados+"/meta/"+id, &mr)
198+	code, err := h.upstream_json("/meta/"+id, r, &mr)
199 	if err != nil {
200 		http.Error(w, "Upstream Error", 502)
201 		return
202 	}
203+
204+	if code == 401 {
205+		w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
206+		http.Error(w, "Unauthorized", 401)
207+		return
208+	}
209+
210 	if code != 200 {
211 		http.Error(w, http.StatusText(code), code)
212 		return
213 	}
214+
215 	if mr.Proto != 1 {
216 		http.Error(w, "Unknown Proto Version", 502)
217 		return
218 	}
219 
220-	w.Header().Set("Content-Type", "text/html; charset=utf-8")
221-	fmt.Fprintf(w, "<!doctype html><meta charset=utf-8><title>%s</title>", mr.Name)
222-	fmt.Fprintf(w, "<h1>%s</h1>", mr.Name)
223-	fmt.Fprintf(w, "<p><code>%s</code></p>", mr.Path)
224-	fmt.Fprintf(w, "<ul>")
225-	fmt.Fprintf(w, "<li>id: <code>%s</code></li>", mr.ID)
226-	fmt.Fprintf(w, "<li>type: <code>%s</code></li>", mr.Type)
227-	fmt.Fprintf(w, "<li>kind: <code>%s</code></li>", mr.Kind)
228-	fmt.Fprintf(w, "<li>size: %d</li>", mr.Size)
229-	fmt.Fprintf(w, "<li>mtime: %d</li>", mr.Mtime)
230-	fmt.Fprintf(w, "</ul>")
231-
232-	// player
233+	body, err := h.load_web("item.html")
234+	if err != nil {
235+		http.Error(w, "Web Error: "+h.web_dir+"/item.html: "+err.Error(), 500)
236+		return
237+	}
238+
239+	var meta strings.Builder
240+	fmt.Fprintf(&meta, "<li>id: <code>%s</code></li>\n", mr.ID)
241+	fmt.Fprintf(&meta, "<li>type: <code>%s</code></li>\n", mr.Type)
242+	fmt.Fprintf(&meta, "<li>kind: <code>%s</code></li>\n", mr.Kind)
243+	fmt.Fprintf(&meta, "<li>size: %d</li>\n", mr.Size)
244+	fmt.Fprintf(&meta, "<li>mtime: %d</li>\n", mr.Mtime)
245+
246+	player := ""
247 	if mr.Kind == "video" {
248-		fmt.Fprintf(w, `<video controls preload="metadata" style="max-width: 100%%;" src="/stream/%s"></video>`, mr.ID)
249+		player = fmt.Sprintf(`<video controls preload="metadata" src="/stream/%s"></video>`, mr.ID)
250 
251 	} else if mr.Kind == "audio" {
252-		fmt.Fprintf(w, `<audio controls preload="metadata" style="width: 100%%;" src="/stream/%s"></audio>`, mr.ID)
253+		player = fmt.Sprintf(`<audio controls preload="metadata" src="/stream/%s"></audio>`, mr.ID)
254 
255 	} else if mr.Kind == "image" {
256-		fmt.Fprintf(w, `<p><img style="max-width: 100%%;" src="/stream/%s"></p>`, mr.ID)
257+		player = fmt.Sprintf(`<p><img src="/stream/%s"></p>`, mr.ID)
258 
259 	} else {
260-		fmt.Fprintf(w, `<p><a href="/stream/%s">download</a></p>`, mr.ID)
261+		player = fmt.Sprintf(`<p><a href="/stream/%s">download</a></p>`, mr.ID)
262 	}
263 
264-	fmt.Fprintf(w, `<p><a href="/library">back</a> | <a href="/stream/%s">stream</a></p>`, mr.ID)
265+	h.web_subst(w, body, map[string]string{
266+		"{{ID}}": mr.ID,
267+		"{{NAME}}": mr.Name,
268+		"{{PATH}}": mr.Path,
269+		"{{META}}": meta.String(),
270+		"{{PLAYER}}": player,
271+	})
272 }
273 
274 func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
275@@ -155,19 +264,13 @@ func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
276 		return
277 	}
278 
279-	// build upstream req
280-	req, _ := http.NewRequest(r.Method, h.parados+"/stream/"+id, nil)
281-	if h.user != "" {
282-		req.SetBasicAuth(h.user, h.pass)
283-	}
284+	req := h.upstream_req(r.Method, "/stream/"+id, r)
285 
286-	// pass through Range for seeking
287 	rng := r.Header.Get("Range")
288 	if rng != "" {
289 		req.Header.Set("Range", rng)
290 	}
291 
292-	// call upstream
293 	resp, err := h.httpc.Do(req)
294 	if err != nil {
295 		http.Error(w, "Upstream Error", 502)
296@@ -175,7 +278,12 @@ func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
297 	}
298 	defer resp.Body.Close()
299 
300-	// copy important headers for playback
301+	if resp.StatusCode == 401 {
302+		w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
303+		http.Error(w, "Unauthorized", 401)
304+		return
305+	}
306+
307 	ct := resp.Header.Get("Content-Type")
308 	if ct != "" {
309 		w.Header().Set("Content-Type", ct)
310@@ -201,44 +309,38 @@ func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
311 		w.Header().Set("Content-Disposition", cd)
312 	}
313 
314-	// mirror status (200/206/400/416/401/404/etc)
315 	w.WriteHeader(resp.StatusCode)
316 
317-	// HEAD: no body
318 	if r.Method == "HEAD" {
319 		return
320 	}
321 
322-	// stream body
323 	io.Copy(w, resp.Body)
324 }
325 
326 func main() {
327-	listen := flag.String("listen", "127.0.0.1:8808", "Listen Addr")
328-	par := flag.String("parados", "http://127.0.0.1:8088", "Parados Base URL")
329+	listen := flag.String("listen", "0.0.0.0:8808", "Listen Addr")
330+	par := flag.String("parados", "http://127.0.0.1:8088", "Parados URL")
331+	web := flag.String("web", "web", "Web-Content Dir")
332 	flag.Parse()
333 
334-	user := "admin"
335-	pass := "123"
336-
337 	h := &handler{
338 		parados: *par,
339-		user:    user,
340-		pass:    pass,
341-		httpc:   &http.Client{Timeout: 15 * time.Second},
342+		web_dir: *web,
343+		httpc: &http.Client{Timeout: 15 * time.Second},
344 	}
345 
346 	// routes
347 	mux := http.NewServeMux()
348-	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
349-		http.Redirect(w, r, "/library", http.StatusSeeOther)
350-	})
351+	mux.HandleFunc("/", h.index)
352+	mux.HandleFunc("/style.css", h.style)
353+	mux.HandleFunc("/ping", h.ping)
354 	mux.HandleFunc("/library", h.library)
355 	mux.HandleFunc("/item/", h.item)
356 	mux.HandleFunc("/stream/", h.stream)
357 
358 	// start server
359-	fmt.Fprintf(os.Stderr, "gorados: listening on %s\n", *listen)
360+	fmt.Fprintf(os.Stderr, "listening on %s\n", *listen)
361 	if err := http.ListenAndServe(*listen, mux); err != nil {
362 		fmt.Fprintln(os.Stderr, err)
363 		os.Exit(1)
+18, -0
 1@@ -0,0 +1,18 @@
 2+<!doctype html>
 3+<meta charset="utf-8">
 4+<meta name="viewport" content="width=device-width, initial-scale=1">
 5+<title>parados</title>
 6+<link rel="stylesheet" href="/style.css">
 7+
 8+<header>
 9+	<h1>parados</h1>
10+	<p>gorados web client</p>
11+</header>
12+
13+<main>
14+	<ul>
15+		<li><a href="/library">Library</a></li>
16+		<li><a href="/ping">Ping</a></li>
17+	</ul>
18+</main>
19+
+3, -2
 1@@ -7,11 +7,12 @@
 2 <header>
 3 	<h1>parados</h1>
 4 	<p>items: {{COUNT}}</p>
 5+	<p><a href="/">home</a></p>
 6 </header>
 7 
 8 <main>
 9-	<l>
10+	<ul>
11 		{{ITEMS}}
12-	</l>
13+	</ul>
14 </main>
15 
+1, -21
 1@@ -1,9 +1,7 @@
 2-/* gorados */
 3 body {
 4 	color: #eeeeee;
 5 	background-color: #1e1f21;
 6 	font-family: serif;
 7-	margin: 20px;
 8 }
 9 
10 video {
11@@ -13,29 +11,11 @@ video {
12 	display: block;
13 }
14 
15-header {
16-	padding: 12px 16px;
17-	border-bottom: 1px solid #333;
18-}
19-
20-main {
21-	padding: 16px;
22-	max-width: 1000px;
23-}
24-
25 code {
26 	font-family: monospace;
27 }
28 
29 a {
30-	color: inherit;
31-}
32-
33-ul {
34-	padding-left: 18px;
35-}
36-
37-li {
38-	margin: 4px 0;
39+	color: #eeeeee;
40 }
41