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)