master uint/parados / clients / go / client.go
  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