Web UI: setup wizard, auth pages, library browsing, player, admin, streaming, cover art
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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" {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user