package server import ( "errors" "net/http" "os" "path/filepath" "time" "music-server/internal/backend" "music-server/internal/logging" "github.com/labstack/echo/v5" "go.uber.org/zap" ) // serveFile sends a file with support for Range requests and caching headers. // contentType may be empty to detect it from the file extension. func serveFile(c *echo.Context, f *os.File, contentType string) error { defer f.Close() info, err := f.Stat() if err != nil { return err } if info.IsDir() { return echo.NewHTTPError(http.StatusNotFound, "Not Found") } if contentType != "" { c.Response().Header().Set(echo.HeaderContentType, contentType) } http.ServeContent(c.Response(), c.Request(), filepath.Base(f.Name()), info.ModTime(), f) return nil } // serveSong streams a song. The write deadline is lifted because songs can take long to send to slow clients. func serveSong(c *echo.Context, path string, err error) error { if err != nil { return songError(err) } f, err := os.Open(path) if err != nil { return echo.NewHTTPError(http.StatusNotFound, "Not Found") } _ = http.NewResponseController(c.Response()).SetWriteDeadline(time.Time{}) return serveFile(c, f, "audio/mpeg") } // songError maps backend errors to HTTP errors. func songError(err error) error { switch { case errors.Is(err, backend.ErrQueueEmpty), errors.Is(err, backend.ErrNoSongs), errors.Is(err, backend.ErrNotFound): return echo.NewHTTPError(http.StatusNotFound, err.Error()) case errors.Is(err, backend.ErrInvalidName): return echo.NewHTTPError(http.StatusBadRequest, err.Error()) case errors.Is(err, backend.ErrUpstream): return echo.NewHTTPError(http.StatusBadGateway, err.Error()) default: logging.GetLogger().Error("Request failed", zap.Error(err)) return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") } }