102 lines
3.0 KiB
Go
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
|
|
}
|