new-release.go (5282B)
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 newrelease "lyrics/internal/server/ui/views/new-release" 11 "net/http" 12 "time" 13 ) 14 15 type NewReleaseForm struct { 16 Title string `schema:"release-title"` 17 Type domain.ReleaseType `schema:"type"` 18 LatinTitle string `schema:"latin-title"` 19 ArtistID []domain.ArtistID `schema:"artist-id"` 20 ReleaseDate string `schema:"release-date"` 21 MusicBrainzReleaseID domain.MusicBrainzID `schema:"musicbrainz-release-id"` 22 } 23 24 func parseNewReleaseForm(form NewReleaseForm) (domain.NewRelease, error) { 25 var release domain.NewRelease 26 27 // type 28 if form.Type == "" { 29 return release, errors.New("Type must not be empty") 30 } 31 if _, ok := domain.ReleaseTypes[form.Type]; !ok { 32 return release, errors.New("Invalid type selected") 33 } 34 35 // title 36 if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) { 37 return release, errors.New("Title name must not be empty or have leading/trailing whitespace") 38 } 39 40 // artists 41 if len(form.ArtistID) == 0 { 42 return release, errors.New("At least one artist is required") 43 } 44 45 artists := make([]domain.NewReleaseArtist, 0, len(form.ArtistID)) 46 seenArtists := make(map[int64]struct{}, len(form.ArtistID)) 47 48 for i, id := range form.ArtistID { 49 if id < 0 { 50 return release, errors.New("Invalid artist selection") 51 } 52 if _, exists := seenArtists[int64(id)]; exists { 53 return release, errors.New("An artist cannot be added more than once") 54 } 55 seenArtists[int64(id)] = struct{}{} 56 57 artists = append(artists, domain.NewReleaseArtist{ 58 ArtistID: id, 59 Role: domain.RolePrimary, 60 SortOrder: i, 61 }) 62 } 63 64 // latin title 65 var latinTitle *string 66 if form.LatinTitle != "" { 67 if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinTitle) { 68 return release, errors.New("Latin title must not be empty or have leading/trailing whitespace") 69 } 70 if !domain.IsValidSortNameRegex.MatchString(form.LatinTitle) { 71 return release, errors.New("Latin title contains invalid characters") 72 } 73 latinTitle = &form.LatinTitle 74 } 75 76 // release date 77 releaseDate, err := time.Parse("2006-01-02", form.ReleaseDate) 78 if err != nil { 79 return release, fmt.Errorf("Invalid release date: %w", err) 80 } 81 minDate := time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC) 82 maxDate := time.Now().AddDate(3, 0, 0) 83 if releaseDate.Before(minDate) || releaseDate.After(maxDate) { 84 return release, fmt.Errorf("Release date must be between %s and %s", 85 minDate.Format("2006-01-02"), maxDate.Format("2006-01-02")) 86 } 87 88 // musicbrainz release id 89 var musicbrainzReleaseID *domain.MusicBrainzID 90 if form.MusicBrainzReleaseID != "" { 91 if !domain.MusicBrainzIDIsValid(string(form.MusicBrainzReleaseID)) { 92 return release, fmt.Errorf("'%s' must be a valid MusicBrainz ID", form.MusicBrainzReleaseID) 93 } 94 musicbrainzReleaseID = &form.MusicBrainzReleaseID 95 } 96 97 release = domain.NewRelease{ 98 MusicBrainzReleaseID: musicbrainzReleaseID, 99 Title: form.Title, 100 LatinTitle: latinTitle, 101 ReleaseDate: releaseDate, 102 Artists: artists, 103 Type: form.Type, 104 } 105 106 return release, nil 107 } 108 109 func (ss *ServerState) NewRelease(w http.ResponseWriter, r *http.Request) { 110 logger := ss.logger.With("handler", "NewRelease") 111 112 switch r.Method { 113 case http.MethodGet: 114 err := newrelease.NewRelease(newrelease.NewReleaseParams{}).Render(r.Context(), w) 115 if err != nil { 116 if errors.Is(err, context.Canceled) { 117 return 118 } 119 120 logger.Error("failed to render new release page", "err", err) 121 http.Error(w, "Internal Server Error", http.StatusInternalServerError) 122 return 123 } 124 case http.MethodPost: 125 err := r.ParseForm() 126 if err != nil { 127 logger.Error("failed to parse http form", "err", err) 128 htmx.HxError(w, http.StatusBadRequest, "Failed to add release.") 129 return 130 } 131 132 var newReleaseForm NewReleaseForm 133 err = decoder.Decode(&newReleaseForm, r.PostForm) 134 if err != nil { 135 logger.Error("failed to decode http form", "err", err) 136 htmx.HxError(w, http.StatusBadRequest, err.Error()) 137 return 138 } 139 140 newRelease, err := parseNewReleaseForm(newReleaseForm) 141 if err != nil { 142 logger.Error("failed to parse new release form", "err", err) 143 htmx.HxError(w, http.StatusBadRequest, err.Error()) 144 return 145 } 146 147 tx, err := ss.db.BeginTx(r.Context(), nil) 148 if err != nil { 149 logger.Error("failed to begin db transaction", "err", err) 150 htmx.HxError(w, http.StatusInternalServerError, "Failed to add release.") 151 return 152 } 153 154 _, err = db.InsertRelease(tx, newRelease) 155 if err != nil { 156 if rollbackErr := tx.Rollback(); rollbackErr != nil { 157 logger.Error("failed to rollback db transaction", "err", rollbackErr) 158 } 159 logger.Error("failed to save new release into db", "err", err) 160 htmx.HxError(w, http.StatusInternalServerError, "Failed to add release.") 161 return 162 } 163 164 if err := tx.Commit(); err != nil { 165 logger.Error("failed to commit db transaction", "err", err) 166 htmx.HxError(w, http.StatusInternalServerError, "Failed to add release.") 167 return 168 } 169 170 logger.Info("added release", "release", newRelease) 171 172 htmx.HxRedirect(w, http.StatusOK, "/") 173 } 174 }