Files
vibesonic/internal/web/coverart_handler.go
T
2026-08-23 14:13:18 -05:00

102 lines
3.0 KiB
Go

package web
import (
"context"
"net/http"
"os"
"strconv"
"time"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
"git.noroutes.net/NoRoutes/vibetunes/internal/metadata"
"git.noroutes.net/NoRoutes/vibetunes/internal/streaming"
)
const maxCoverArtSize = 1200
// handleCoverArt serves a resized cover image, generating and caching it
// on first request. Authorization: the caller must already be signed in
// (enforced by the router's RequireUser wrapper on this route).
func (h *Web) handleCoverArt(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
size := clampSize(r.URL.Query().Get("size"))
art, err := database.GetCoverArt(h.DB, id)
if err != nil {
http.NotFound(w, r)
return
}
sourcePath, contentHash, isTemp, err := h.resolveCoverArtSource(r.Context(), art)
if err != nil {
http.Error(w, "cover art unavailable", http.StatusNotFound)
return
}
if isTemp {
defer os.Remove(sourcePath)
}
cachedPath, err := h.CoverArt.GetOrCreate(sourcePath, contentHash, size)
if err != nil {
http.Error(w, "could not generate cover art", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
if err := streaming.ServeFile(w, r, cachedPath, "cover.jpg"); err != nil {
http.Error(w, "could not read cover art", http.StatusInternalServerError)
}
}
// resolveCoverArtSource returns a plain image file path for a cover_art
// row, extracting embedded artwork to a temp file on demand. isTemp
// tells the caller whether it must remove the returned path afterward.
func (h *Web) resolveCoverArtSource(ctx context.Context, art *database.CoverArt) (path, contentHash string, isTemp bool, err error) {
if art.Source == "folder" || art.Source == "external" {
// "external" is a locally-downloaded artist photo or an
// admin-uploaded/URL-sourced cover override — already a plain
// file on disk, same as a folder image.
return art.SourcePath, art.ContentHash, false, nil
}
// Embedded: extract the attached-picture stream from the source
// media file each time it's needed for resizing (the resize cache
// below avoids repeating this on every request).
mf, err := database.GetMediaFile(h.DB, art.MediaFileID.String)
if err != nil {
return "", "", false, err
}
folder, err := database.GetMusicFolder(h.DB, mf.MusicFolderID)
if err != nil {
return "", "", false, err
}
absPath, err := streaming.ResolvePath(folder.Path, mf.RelativePath)
if err != nil {
return "", "", false, err
}
prober := &metadata.Prober{FFprobePath: h.Config.FFprobePath}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
tags, err := prober.Probe(ctx, absPath)
if err != nil || !tags.HasEmbeddedArt {
return "", "", false, err
}
extracted, err := h.CoverArt.ExtractEmbedded(ctx, absPath, tags.EmbeddedArtStream)
if err != nil {
return "", "", false, err
}
return extracted, art.ContentHash, true, nil
}
func clampSize(raw string) int {
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return 300
}
if n > maxCoverArtSize {
return maxCoverArtSize
}
return n
}