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