196 lines
6.0 KiB
Go
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
|
|
}
|