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 }