edit-artist.go (5098B)
1 package server 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "lyrics/internal/db" 8 "lyrics/internal/domain" 9 "lyrics/internal/server/htmx" 10 "lyrics/internal/server/routes" 11 editartist "lyrics/internal/server/ui/views/edit-artist" 12 "net/http" 13 "strconv" 14 ) 15 16 type EditArtistForm struct { 17 Name string `schema:"artist"` 18 Bio string `schema:"biography"` 19 LatinName string `schema:"latin-name"` 20 Disambiguation string `schema:"disambiguation"` 21 MusicBrainzArtistID domain.MusicBrainzID `schema:"musicbrainz-artist-id"` 22 } 23 24 func parseEditArtistForm(form EditArtistForm) (domain.Artist, error) { 25 var artist domain.Artist 26 27 // name 28 if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Name) { 29 return artist, errors.New("Artist name must not be empty or have leading/trailing whitespace") 30 } 31 32 // latin name 33 var latinName *string 34 if form.LatinName != "" { 35 if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinName) { 36 return artist, errors.New("Latin name must not be empty or have leading/trailing whitespace") 37 } 38 if !domain.IsValidSortNameRegex.MatchString(form.LatinName) { 39 return artist, errors.New("Latin name contains invalid characters") 40 } 41 latinName = &form.LatinName 42 } 43 44 // bio 45 var bio *string 46 if form.Bio != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Bio) { 47 return artist, errors.New("Artist biography must not be empty or have leading/trailing whitespace") 48 } 49 if form.Bio != "" { 50 bio = &form.Bio 51 } 52 53 // disambiguation 54 var disambiguation *string 55 if form.Disambiguation != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Disambiguation) { 56 return artist, errors.New("Artist disambiguation must not be empty or have leading/trailing whitespace") 57 } 58 if form.Disambiguation != "" { 59 disambiguation = &form.Disambiguation 60 } 61 62 // musicbrainz artist id 63 var musicbrainzArtistID *domain.MusicBrainzID 64 if form.MusicBrainzArtistID != "" { 65 if !domain.MusicBrainzIDIsValid(string(form.MusicBrainzArtistID)) { 66 return artist, fmt.Errorf("'%s' must be a valid MusicBrainz ID", form.MusicBrainzArtistID) 67 } 68 musicbrainzArtistID = &form.MusicBrainzArtistID 69 } 70 71 artist = domain.Artist{ 72 Name: form.Name, 73 LatinName: latinName, 74 Bio: bio, 75 Disambiguation: disambiguation, 76 MusicBrainzArtistID: musicbrainzArtistID, 77 } 78 79 return artist, nil 80 } 81 82 func (ss *ServerState) EditArtist(w http.ResponseWriter, r *http.Request) { 83 logger := ss.logger.With("handler", "EditArtist") 84 85 idStr := r.PathValue("id") 86 id, err := strconv.Atoi(idStr) 87 if err != nil { 88 logger.Error("failed to convert artist id to integer", "id", idStr, "err", err) 89 htmx.HxError(w, http.StatusBadRequest, err.Error()) 90 return 91 } 92 93 slug := r.PathValue("slug") 94 logger = logger.With("id", id, "slug", slug) 95 96 switch r.Method { 97 case http.MethodGet: 98 artist, _, _, _, err := db.Artist(ss.db, domain.ArtistID(id)) 99 if err != nil { 100 logger.Error("failed to get artist", "err", err) 101 htmx.HxError(w, http.StatusInternalServerError, fmt.Sprintf("failed to query for artist '%s'", slug)) 102 return 103 } 104 105 err = editartist.EditArtist(editartist.EditArtistParams{ 106 Artist: artist, 107 }).Render(r.Context(), w) 108 if err != nil { 109 if errors.Is(err, context.Canceled) { 110 return 111 } 112 113 logger.Error("failed to render edit artist page", "err", err) 114 http.Error(w, "Internal Server Error", http.StatusInternalServerError) 115 return 116 } 117 case http.MethodPost: 118 err := r.ParseForm() 119 if err != nil { 120 logger.Error("failed to parse http form", "err", err) 121 htmx.HxError(w, http.StatusBadRequest, "Failed to edit artist.") 122 return 123 } 124 125 var editArtistForm EditArtistForm 126 err = decoder.Decode(&editArtistForm, r.PostForm) 127 if err != nil { 128 logger.Error("failed to decode http form", "err", err) 129 htmx.HxError(w, http.StatusBadRequest, err.Error()) 130 return 131 } 132 133 editedArtist, err := parseEditArtistForm(editArtistForm) 134 if err != nil { 135 logger.Error("failed to parse edit artist form", "err", err) 136 htmx.HxError(w, http.StatusBadRequest, err.Error()) 137 return 138 } 139 editedArtist.ID = domain.ArtistID(id) 140 141 tx, err := ss.db.BeginTx(r.Context(), nil) 142 if err != nil { 143 logger.Error("failed to begin db transaction", "err", err) 144 htmx.HxError(w, http.StatusInternalServerError, "Failed to edit artist.") 145 return 146 } 147 148 err = db.UpdateArtist(tx, editedArtist) 149 if err != nil { 150 if rollbackErr := tx.Rollback(); rollbackErr != nil { 151 logger.Error("failed to rollback db transaction", "err", rollbackErr) 152 } 153 logger.Error("failed to save edited artist into db", "err", err) 154 htmx.HxError(w, http.StatusInternalServerError, "Failed to edit artist.") 155 return 156 } 157 158 if err := tx.Commit(); err != nil { 159 logger.Error("failed to commit db transaction", "err", err) 160 htmx.HxError(w, http.StatusInternalServerError, "Failed to edit artist.") 161 return 162 } 163 164 logger.Info("edited artist", "artist", editedArtist) 165 166 htmx.HxRedirect(w, http.StatusOK, routes.Artist(editedArtist)) 167 } 168 }