commit 08b5697

uint  ·  2026-02-01 01:28:04 +0000 UTC
parent 1aa0c2b
gorados: conform to go-formatting, add streaming
1 files changed,  +111, -20
+111, -20
  1@@ -1,9 +1,14 @@
  2+// gorados
  3+// is a Go frontend for the parados
  4+// HTTP media server
  5+
  6 package main
  7 
  8 import (
  9 	"encoding/json"
 10 	"flag"
 11 	"fmt"
 12+	"io"
 13 	"net/http"
 14 	"os"
 15 	"strings"
 16@@ -12,37 +17,37 @@ import (
 17 
 18 // library item
 19 type item struct {
 20-	ID string      `json:"id"`
 21-	Path string    `json:"path"`
 22+	ID   string `json:"id"`
 23+	Path string `json:"path"`
 24 }
 25 
 26 // upstream response
 27 type library_resp struct {
 28-	Proto int      `json:"proto"`
 29-	Items []item   `json:"items"`
 30+	Proto int    `json:"proto"`
 31+	Items []item `json:"items"`
 32 }
 33 
 34 // /meta/{id} response
 35 type meta_resp struct {
 36-	Proto int      `json:"proto"`
 37-	ID string      `json:"id"`
 38-	Path string    `json:"path"`
 39-	Name string    `json:"name"`
 40-	Size uint64    `json:"size"`
 41-	Mtime int64    `json:"mtime"`
 42-	Type string    `json:"type"`
 43-	Kind string    `json:"kind"`
 44+	Proto int    `json:"proto"`
 45+	ID    string `json:"id"`
 46+	Path  string `json:"path"`
 47+	Name  string `json:"name"`
 48+	Size  uint64 `json:"size"`
 49+	Mtime int64  `json:"mtime"`
 50+	Type  string `json:"type"`
 51+	Kind  string `json:"kind"`
 52 }
 53 
 54 // app state
 55 type handler struct {
 56-	parados        string
 57-	user           string
 58-	pass           string
 59-	httpc*         http.Client
 60+	parados string
 61+	user    string
 62+	pass    string
 63+	httpc   *http.Client
 64 }
 65 
 66-func (h* handler) get_json(url string, out any) (int, error) {
 67+func (h *handler) get_json(url string, out any) (int, error) {
 68 	req, _ := http.NewRequest("GET", url, nil)
 69 	if h.user != "" {
 70 		req.SetBasicAuth(h.user, h.pass)
 71@@ -61,7 +66,7 @@ func (h* handler) get_json(url string, out any) (int, error) {
 72 	return 200, json.NewDecoder(resp.Body).Decode(out)
 73 }
 74 
 75-func (h* handler) library(w http.ResponseWriter, r *http.Request) {
 76+func (h *handler) library(w http.ResponseWriter, r *http.Request) {
 77 	var lr library_resp
 78 	code, err := h.get_json(h.parados+"/library", &lr)
 79 	if err != nil {
 80@@ -87,7 +92,7 @@ func (h* handler) library(w http.ResponseWriter, r *http.Request) {
 81 	fmt.Fprintf(w, "</ul>")
 82 }
 83 
 84-func (h* handler) item(w http.ResponseWriter, r *http.Request) {
 85+func (h *handler) item(w http.ResponseWriter, r *http.Request) {
 86 	id := strings.TrimPrefix(r.URL.Path, "/item/")
 87 	if id == "" {
 88 		http.NotFound(w, r)
 89@@ -120,7 +125,92 @@ func (h* handler) item(w http.ResponseWriter, r *http.Request) {
 90 	fmt.Fprintf(w, "<li>size: %d</li>", mr.Size)
 91 	fmt.Fprintf(w, "<li>mtime: %d</li>", mr.Mtime)
 92 	fmt.Fprintf(w, "</ul>")
 93-	fmt.Fprintf(w, `<p><a href="/library">back</a> | <a href="/stream/%s">stream</a></p>`, mr.ID) // TODO
 94+
 95+	// player
 96+	if mr.Kind == "video" {
 97+		fmt.Fprintf(w, `<video controls preload="metadata" style="max-width: 100%%;" src="/stream/%s"></video>`, mr.ID)
 98+
 99+	} else if mr.Kind == "audio" {
100+		fmt.Fprintf(w, `<audio controls preload="metadata" style="width: 100%%;" src="/stream/%s"></audio>`, mr.ID)
101+
102+	} else if mr.Kind == "image" {
103+		fmt.Fprintf(w, `<p><img style="max-width: 100%%;" src="/stream/%s"></p>`, mr.ID)
104+
105+	} else {
106+		fmt.Fprintf(w, `<p><a href="/stream/%s">download</a></p>`, mr.ID)
107+	}
108+
109+	fmt.Fprintf(w, `<p><a href="/library">back</a> | <a href="/stream/%s">stream</a></p>`, mr.ID)
110+}
111+
112+func (h *handler) stream(w http.ResponseWriter, r *http.Request) {
113+	id := strings.TrimPrefix(r.URL.Path, "/stream/")
114+	if id == "" {
115+		http.NotFound(w, r)
116+		return
117+	}
118+
119+	if r.Method != "GET" && r.Method != "HEAD" {
120+		http.Error(w, "Method Not Allowed", 405)
121+		return
122+	}
123+
124+	// build upstream req
125+	req, _ := http.NewRequest(r.Method, h.parados+"/stream/"+id, nil)
126+	if h.user != "" {
127+		req.SetBasicAuth(h.user, h.pass)
128+	}
129+
130+	// pass through Range for seeking
131+	rng := r.Header.Get("Range")
132+	if rng != "" {
133+		req.Header.Set("Range", rng)
134+	}
135+
136+	// call upstream
137+	resp, err := h.httpc.Do(req)
138+	if err != nil {
139+		http.Error(w, "Upstream Error", 502)
140+		return
141+	}
142+	defer resp.Body.Close()
143+
144+	// copy important headers for playback
145+	ct := resp.Header.Get("Content-Type")
146+	if ct != "" {
147+		w.Header().Set("Content-Type", ct)
148+	}
149+
150+	cl := resp.Header.Get("Content-Length")
151+	if cl != "" {
152+		w.Header().Set("Content-Length", cl)
153+	}
154+
155+	cr := resp.Header.Get("Content-Range")
156+	if cr != "" {
157+		w.Header().Set("Content-Range", cr)
158+	}
159+
160+	ar := resp.Header.Get("Accept-Ranges")
161+	if ar != "" {
162+		w.Header().Set("Accept-Ranges", ar)
163+	}
164+
165+	cd := resp.Header.Get("Content-Disposition")
166+	if cd != "" {
167+		w.Header().Set("Content-Disposition", cd)
168+	}
169+
170+	// mirror status (200/206/400/416/401/404/etc)
171+	w.WriteHeader(resp.StatusCode)
172+
173+	// HEAD: no body
174+	if r.Method == "HEAD" {
175+		return
176+	}
177+
178+	// stream body
179+	io.Copy(w, resp.Body)
180 }
181 
182 func main() {
183@@ -145,6 +235,7 @@ func main() {
184 	})
185 	mux.HandleFunc("/library", h.library)
186 	mux.HandleFunc("/item/", h.item)
187+	mux.HandleFunc("/stream/", h.stream)
188 
189 	// start server
190 	fmt.Fprintf(os.Stderr, "gorados: listening on %s\n", *listen)