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

196 lines
6.0 KiB
Go

package web
import (
"database/sql"
"io"
"net/http"
"strconv"
"strings"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
"git.noroutes.net/NoRoutes/vibetunes/internal/netguard"
)
// --- Artists ---
func (h *Web) handleAdminArtistEdit(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
artist, err := database.GetArtist(h.DB, id)
if err != nil {
http.NotFound(w, r)
return
}
h.page(w, r, "admin_artist_edit", "artists", "Edit artist: "+artist.Name, artist)
}
func (h *Web) handleAdminArtistUpdate(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if _, err := database.GetArtist(h.DB, id); err != nil {
http.NotFound(w, r)
return
}
if err := r.ParseMultipartForm(maxArtistPhotoBytes); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := database.UpdateArtist(h.DB, id, r.FormValue("name"), r.FormValue("sort_name"), r.FormValue("bio")); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
switch {
case r.FormValue("remove_photo") == "1":
if err := database.SetArtistCoverArt(h.DB, id, sql.NullString{}, false); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
default:
if !h.applyPhotoOverride(w, r, "photo_file", "photo_url", func(localPath, contentHash, mimeType string) error {
_, err := database.UpsertArtistCoverArt(h.DB, id, "external", localPath, mimeType, contentHash, 0, 0, true)
return err
}) {
return
}
}
http.Redirect(w, r, "/artists/"+id, http.StatusFound)
}
// --- Albums ---
func (h *Web) handleAdminAlbumEdit(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
album, err := database.GetAlbum(h.DB, id)
if err != nil {
http.NotFound(w, r)
return
}
h.page(w, r, "admin_album_edit", "albums", "Edit album: "+album.Title, album)
}
func (h *Web) handleAdminAlbumUpdate(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if _, err := database.GetAlbum(h.DB, id); err != nil {
http.NotFound(w, r)
return
}
if err := r.ParseMultipartForm(maxArtistPhotoBytes); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := database.UpdateAlbum(h.DB, id, r.FormValue("title"), r.FormValue("sort_title"),
formNullInt(r, "year"), formNullInt(r, "original_year"), r.FormValue("record_label")); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if !h.applyPhotoOverride(w, r, "cover_file", "cover_url", func(localPath, contentHash, mimeType string) error {
_, err := database.UpsertAlbumCoverArt(h.DB, id, "external", localPath, "", mimeType, contentHash, 0, 0)
return err
}) {
return
}
http.Redirect(w, r, "/albums/"+id, http.StatusFound)
}
// --- Songs ---
func (h *Web) handleAdminSongEdit(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
song, err := database.GetSong(h.DB, id)
if err != nil {
http.NotFound(w, r)
return
}
h.page(w, r, "admin_song_edit", "songs", "Edit song: "+song.Title, song)
}
func (h *Web) handleAdminSongUpdate(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if _, err := database.GetSong(h.DB, id); err != nil {
http.NotFound(w, r)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
locked := r.FormValue("metadata_locked") == "1"
if err := database.UpdateSongMetadata(h.DB, id, r.FormValue("title"), r.FormValue("sort_title"),
formNullInt(r, "track_number"), formNullInt(r, "disc_number"), r.FormValue("composer"), r.FormValue("conductor"), locked); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/songs", http.StatusFound)
}
// --- shared helpers ---
// formNullInt parses a form field as an integer, returning an invalid
// sql.NullInt64 when the field is blank or unparsable rather than
// erroring the request — the same tolerant behavior as the rest of
// VibeSonic's admin forms.
func formNullInt(r *http.Request, name string) sql.NullInt64 {
raw := strings.TrimSpace(r.FormValue(name))
if raw == "" {
return sql.NullInt64{}
}
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return sql.NullInt64{}
}
return sql.NullInt64{Int64: n, Valid: true}
}
// applyPhotoOverride implements the common "upload a file, or give a
// URL, or leave the existing photo/cover alone" logic shared by the
// artist and album edit forms. store is called with the resulting local
// file's path/hash/mime once one is available. Returns false (having
// already written an HTTP error response) on failure.
func (h *Web) applyPhotoOverride(w http.ResponseWriter, r *http.Request, fileField, urlField string, store func(localPath, contentHash, mimeType string) error) bool {
if file, _, err := r.FormFile(fileField); err == nil {
defer file.Close()
body, err := io.ReadAll(io.LimitReader(file, maxArtistPhotoBytes+1))
if err != nil {
http.Error(w, "could not read uploaded image", http.StatusBadRequest)
return false
}
if len(body) > maxArtistPhotoBytes {
http.Error(w, "uploaded image exceeds maximum allowed size", http.StatusBadRequest)
return false
}
localPath, contentHash, mimeType, err := h.storeImageBytes(body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return false
}
if err := store(localPath, contentHash, mimeType); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return false
}
return true
}
photoURL := strings.TrimSpace(r.FormValue(urlField))
if photoURL == "" {
return true
}
if err := netguard.ValidateURL(photoURL); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return false
}
localPath, contentHash, mimeType, err := h.downloadImage(r.Context(), photoURL)
if err != nil {
http.Error(w, "could not download image: "+err.Error(), http.StatusBadRequest)
return false
}
if err := store(localPath, contentHash, mimeType); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return false
}
return true
}