lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

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 }