Files
MusicServer/internal/server/files.go
Sansan 76a1b946eb
CI / docker (push) Successful in 1m6s
CI / test (push) Successful in 22s
CI / integration (push) Successful in 23s
CI / publish (push) Skipped
Stop browsers from caching songs
/music/rand and the other song endpoints return a different song from the
same URL, but the responses had a Last-Modified header and no Cache-Control,
so browsers cached them. The same song played again and the server never
picked a new one, so /music/addQue added nothing. Send Cache-Control:
no-store again, as the server did before the rewrite.

Also allow the Cache-Control request header in CORS, which the frontend
sends to bypass responses that browsers have already cached.
2026-10-07 20:09:30 +02:00

73 lines
2.3 KiB
Go

package server
import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/danielgtaylor/huma/v2"
)
// fileResponses documents an endpoint that returns a file with Range support.
func fileResponses(description string, contentTypes ...string) map[string]*huma.Response {
content := map[string]*huma.MediaType{}
for _, contentType := range contentTypes {
content[contentType] = &huma.MediaType{Schema: &huma.Schema{Type: "string", Format: "binary"}}
}
return map[string]*huma.Response{
"200": {Description: description, Content: content},
"206": {Description: "Part of the file, for a Range request", Content: content},
}
}
// streamFile opens path and returns a response that sends it with Range support.
// contentType may be empty to detect it from the file extension. Audio has no write
// deadline because songs can take long to send to slow clients.
func streamFile(path, contentType string, audio bool) (*huma.StreamResponse, error) {
f, err := os.Open(path)
if err != nil {
return nil, huma.Error404NotFound("Not Found")
}
return streamOpenFile(f, contentType, audio)
}
func streamOpenFile(f *os.File, contentType string, audio bool) (*huma.StreamResponse, error) {
info, err := f.Stat()
if err != nil || info.IsDir() {
f.Close()
return nil, huma.Error404NotFound("Not Found")
}
return &huma.StreamResponse{Body: func(ctx huma.Context) {
defer f.Close()
c := echoContext(ctx)
if contentType != "" {
c.Response().Header().Set("Content-Type", contentType)
}
if audio {
_ = http.NewResponseController(c.Response()).SetWriteDeadline(time.Time{})
}
http.ServeContent(c.Response(), c.Request(), filepath.Base(f.Name()), info.ModTime(), f)
}}, nil
}
// streamSong streams the song at path, or returns err mapped to an API error.
// The same URL returns a different song each time (random songs, next, previous),
// so browsers must not cache it. Without this they cache the response because of
// its Last-Modified header, and play the same song again.
func streamSong(path string, err error) (*huma.StreamResponse, error) {
if err != nil {
return nil, apiError(err)
}
resp, err := streamFile(path, "audio/mpeg", true)
if err != nil {
return nil, err
}
body := resp.Body
resp.Body = func(ctx huma.Context) {
ctx.SetHeader("Cache-Control", "no-store")
body(ctx)
}
return resp, nil
}