feat(api): GET /api/albums/{id}/cover with sidecar fallback

This commit is contained in:
2026-04-21 22:37:53 -04:00
parent 5af6d7fac6
commit 945b773cbd
3 changed files with 192 additions and 0 deletions
+51
View File
@@ -8,10 +8,15 @@ package api
import (
"context"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
@@ -96,3 +101,49 @@ func imageContentType(path string) string {
}
return "application/octet-stream"
}
// handleGetCover implements GET /api/albums/{id}/cover. Resolves the cover
// path (explicit column, else sidecar next to first track), then delegates
// byte-serving to http.ServeContent so clients get Range / If-Modified-Since
// / ETag for free.
func (h *handlers) handleGetCover(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(chi.URLParam(r, "id"))
if !ok {
writeErr(w, http.StatusBadRequest, "bad_request", "invalid album id")
return
}
q := dbq.New(h.pool)
album, err := q.GetAlbumByID(r.Context(), id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
writeErr(w, http.StatusNotFound, "not_found", "album not found")
return
}
h.logger.Error("api: get album for cover failed", "err", err)
writeErr(w, http.StatusInternalServerError, "server_error", "server error")
return
}
path := resolveAlbumCoverPath(r.Context(), q, album)
if path == "" {
writeErr(w, http.StatusNotFound, "not_found", "cover not found")
return
}
f, err := os.Open(path)
if err != nil {
// resolveAlbumCoverPath saw the file a moment ago; if it vanished
// between stat and open, treat it the same as no-art — the user
// experience (404) matches, and 500 would misleadingly imply a server
// bug rather than a filesystem race.
writeErr(w, http.StatusNotFound, "not_found", "cover not found")
return
}
defer func() { _ = f.Close() }()
info, err := f.Stat()
if err != nil {
h.logger.Error("api: stat cover failed", "err", err)
writeErr(w, http.StatusInternalServerError, "server_error", "server error")
return
}
w.Header().Set("Content-Type", imageContentType(path))
http.ServeContent(w, r, filepath.Base(path), info.ModTime(), f)
}