1// Go-Web frontend for the
2// parados HTTP media server
3
4package main
5
6import (
7 "encoding/json"
8 "flag"
9 "fmt"
10 "io"
11 "net/http"
12 "os"
13 "strings"
14 "time"
15)
16
17// structs
18
19// library item
20type item struct {
21 ID string `json:"id"`
22 Path string `json:"path"`
23}
24// upstream response
25type library_resp struct {
26 Proto int `json:"proto"`
27 Items []item `json:"items"`
28}
29// /meta/{id} response
30type meta_resp struct {
31 Proto int `json:"proto"`
32 ID string `json:"id"`
33 Path string `json:"path"`
34 Name string `json:"name"`
35 Size uint64 `json:"size"`
36 Mtime int64 `json:"mtime"`
37 Type string `json:"type"`
38 Kind string `json:"kind"`
39}
40// app state
41type handler struct {
42 parados string
43 web_dir string
44 httpc *http.Client
45}
46
47// functions
48func (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
65func (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
76func (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
86func (h *handler) pass_auth(dst *http.Request, src *http.Request) {
87 a := src.Header.Get("Authorization")
88 if a != "" {
89 dst.Header.Set("Authorization", a)
90 }
91}
92
93func (h *handler) upstream_req(method string, path string, r *http.Request) *http.Request {
94 req, _ := http.NewRequest(method, h.parados+path, nil)
95 h.pass_auth(req, r)
96 return req
97}
98
99func (h *handler) upstream_json(path string, r *http.Request, out any) (int, error) {
100 req := h.upstream_req("GET", path, r)
101
102 resp, err := h.httpc.Do(req)
103 if err != nil {
104 return 0, err
105 }
106 defer resp.Body.Close()
107
108 if resp.StatusCode != 200 {
109 return resp.StatusCode, nil
110 }
111
112 return 200, json.NewDecoder(resp.Body).Decode(out)
113}
114
115func (h *handler) index(w http.ResponseWriter, r *http.Request) {
116 if r.URL.Path != "/" && r.URL.Path != "/index.html" {
117 http.NotFound(w, r)
118 return
119 }
120
121 h.serve_file(w, r, "index.html", "text/html; charset=utf-8")
122}
123
124func (h *handler) style(w http.ResponseWriter, r *http.Request) {
125 h.serve_file(w, r, "style.css", "text/css; charset=utf-8")
126}
127
128func (h *handler) ping(w http.ResponseWriter, r *http.Request) {
129 req := h.upstream_req("GET", "/ping", r)
130
131 resp, err := h.httpc.Do(req)
132 if err != nil {
133 http.Error(w, "Upstream Error", 502)
134 return
135 }
136 defer resp.Body.Close()
137
138 w.WriteHeader(resp.StatusCode)
139
140 if r.Method == "HEAD" {
141 return
142 }
143
144 io.Copy(w, resp.Body)
145}
146
147func (h *handler) library(w http.ResponseWriter, r *http.Request) {
148 var lr library_resp
149 code, err := h.upstream_json("/library", r, &lr)
150 if err != nil {
151 http.Error(w, "Upstream Error", 502)
152 return
153 }
154
155 if code == 401 {
156 w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
157 http.Error(w, "Unauthorized", 401)
158 return
159 }
160
161 if code != 200 {
162 http.Error(w, http.StatusText(code), code)
163 return
164 }
165
166 if lr.Proto != 1 {
167 http.Error(w, "Unknown Proto Version", 502)
168 return
169 }
170
171 body, err := h.load_web("library.html")
172 if err != nil {
173 http.Error(w, "Web Error: "+h.web_dir+"/library.html: "+err.Error(), 500)
174 return
175 }
176
177 var items strings.Builder
178 for _, it := range lr.Items {
179 fmt.Fprintf(&items, `<li><a href="/item/%s">%s</a></li>`+"\n", it.ID, it.Path)
180 }
181
182 h.web_subst(w, body, map[string]string{
183 "{{COUNT}}": fmt.Sprintf("%d", len(lr.Items)),
184 "{{ITEMS}}": items.String(),
185 })
186}
187
188func (h *handler) item(w http.ResponseWriter, r *http.Request) {
189 id := strings.TrimPrefix(r.URL.Path, "/item/")
190 if id == "" {
191 http.NotFound(w, r)
192 return
193 }
194
195 var mr meta_resp
196 code, err := h.upstream_json("/meta/"+id, r, &mr)
197 if err != nil {
198 http.Error(w, "Upstream Error", 502)
199 return
200 }
201
202 if code == 401 {
203 w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
204 http.Error(w, "Unauthorized", 401)
205 return
206 }
207
208 if code != 200 {
209 http.Error(w, http.StatusText(code), code)
210 return
211 }
212
213 if mr.Proto != 1 {
214 http.Error(w, "Unknown Proto Version", 502)
215 return
216 }
217
218 body, err := h.load_web("item.html")
219 if err != nil {
220 http.Error(w, "Web Error: "+h.web_dir+"/item.html: "+err.Error(), 500)
221 return
222 }
223
224 var meta strings.Builder
225 fmt.Fprintf(&meta, "<li>id: <code>%s</code></li>\n", mr.ID)
226 fmt.Fprintf(&meta, "<li>type: <code>%s</code></li>\n", mr.Type)
227 fmt.Fprintf(&meta, "<li>kind: <code>%s</code></li>\n", mr.Kind)
228 fmt.Fprintf(&meta, "<li>size: %d</li>\n", mr.Size)
229 fmt.Fprintf(&meta, "<li>mtime: %d</li>\n", mr.Mtime)
230
231 player := ""
232 if mr.Kind == "video" {
233 player = fmt.Sprintf(`<video controls preload="metadata" src="/stream/%s"></video>`, mr.ID)
234
235 } else if mr.Kind == "audio" {
236 player = fmt.Sprintf(`<audio controls preload="metadata" src="/stream/%s"></audio>`, mr.ID)
237
238 } else if mr.Kind == "image" {
239 player = fmt.Sprintf(`<p><img src="/stream/%s"></p>`, mr.ID)
240
241 } else {
242 player = fmt.Sprintf(`<p><a href="/stream/%s">download</a></p>`, mr.ID)
243 }
244
245 h.web_subst(w, body, map[string]string{
246 "{{ID}}": mr.ID,
247 "{{NAME}}": mr.Name,
248 "{{PATH}}": mr.Path,
249 "{{META}}": meta.String(),
250 "{{PLAYER}}": player,
251 })
252}
253
254func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
255 id := strings.TrimPrefix(r.URL.Path, "/stream/")
256 if id == "" {
257 http.NotFound(w, r)
258 return
259 }
260
261 if r.Method != "GET" && r.Method != "HEAD" {
262 http.Error(w, "Method Not Allowed", 405)
263 return
264 }
265
266 req := h.upstream_req(r.Method, "/stream/"+id, r)
267
268 rng := r.Header.Get("Range")
269 if rng != "" {
270 req.Header.Set("Range", rng)
271 }
272
273 resp, err := h.httpc.Do(req)
274 if err != nil {
275 http.Error(w, "Upstream Error", 502)
276 return
277 }
278 defer resp.Body.Close()
279
280 if resp.StatusCode == 401 {
281 w.Header().Set("WWW-Authenticate", `Basic realm="parados"`)
282 http.Error(w, "Unauthorized", 401)
283 return
284 }
285
286 ct := resp.Header.Get("Content-Type")
287 if ct != "" {
288 w.Header().Set("Content-Type", ct)
289 }
290
291 cl := resp.Header.Get("Content-Length")
292 if cl != "" {
293 w.Header().Set("Content-Length", cl)
294 }
295
296 cr := resp.Header.Get("Content-Range")
297 if cr != "" {
298 w.Header().Set("Content-Range", cr)
299 }
300
301 ar := resp.Header.Get("Accept-Ranges")
302 if ar != "" {
303 w.Header().Set("Accept-Ranges", ar)
304 }
305
306 cd := resp.Header.Get("Content-Disposition")
307 if cd != "" {
308 w.Header().Set("Content-Disposition", cd)
309 }
310
311 w.WriteHeader(resp.StatusCode)
312
313 if r.Method == "HEAD" {
314 return
315 }
316
317 io.Copy(w, resp.Body)
318}
319
320func main() {
321 listen := flag.String("listen", "0.0.0.0:8808", "Listen Addr")
322 par := flag.String("parados", "http://127.0.0.1:8088", "Parados URL")
323 web := flag.String("web", "web", "Web-Content Dir")
324 flag.Parse()
325
326 h := &handler{
327 parados: *par,
328 web_dir: *web,
329 httpc: &http.Client{Timeout: 15 * time.Second},
330 }
331
332 // routes
333 mux := http.NewServeMux()
334 mux.HandleFunc("/", h.index)
335 mux.HandleFunc("/style.css", h.style)
336 mux.HandleFunc("/ping", h.ping)
337 mux.HandleFunc("/library", h.library)
338 mux.HandleFunc("/item/", h.item)
339 mux.HandleFunc("/stream/", h.stream)
340
341 // start server
342 fmt.Fprintf(os.Stderr, "listening on %s\n", *listen)
343 if err := http.ListenAndServe(*listen, mux); err != nil {
344 fmt.Fprintln(os.Stderr, err)
345 os.Exit(1)
346 }
347}
348