lyrics

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

edit-release.go (7342B)


      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 	editrelease "lyrics/internal/server/ui/views/edit-release"
     12 	"net/http"
     13 	"strconv"
     14 	"time"
     15 )
     16 
     17 type EditReleaseForm struct {
     18 	Title                string               `schema:"release-title"`
     19 	Type                 domain.ReleaseType   `schema:"type"`
     20 	LatinTitle           string               `schema:"latin-title"`
     21 	ArtistID             []domain.ArtistID    `schema:"artist-id"`
     22 	SongID               []domain.SongID      `schema:"song-id"`
     23 	TrackNumber          []int                `schema:"track-number"`
     24 	ReleaseDate          string               `schema:"release-date"`
     25 	MusicBrainzReleaseID domain.MusicBrainzID `schema:"musicbrainz-release-id"`
     26 }
     27 
     28 func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) {
     29 	var release domain.EditRelease
     30 
     31 	// type
     32 	if form.Type == "" {
     33 		return release, errors.New("Type must not be empty")
     34 	}
     35 	if _, ok := domain.ReleaseTypes[form.Type]; !ok {
     36 		return release, errors.New("Invalid type selected")
     37 	}
     38 
     39 	// title
     40 	if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) {
     41 		return release, errors.New("Title name must not be empty or have leading/trailing whitespace")
     42 	}
     43 
     44 	// artists
     45 	if len(form.ArtistID) == 0 {
     46 		return release, errors.New("At least one artist is required")
     47 	}
     48 
     49 	artists := make([]domain.NewReleaseArtist, 0, len(form.ArtistID))
     50 	seenArtists := make(map[int64]struct{}, len(form.ArtistID))
     51 
     52 	for i, id := range form.ArtistID {
     53 		if id < 0 {
     54 			return release, errors.New("Invalid artist selection")
     55 		}
     56 		if _, exists := seenArtists[int64(id)]; exists {
     57 			return release, errors.New("An artist cannot be added more than once")
     58 		}
     59 		seenArtists[int64(id)] = struct{}{}
     60 
     61 		artists = append(artists, domain.NewReleaseArtist{
     62 			ArtistID:  id,
     63 			Role:      domain.RolePrimary,
     64 			SortOrder: i,
     65 		})
     66 	}
     67 
     68 	// latin title
     69 	var latinTitle *string
     70 	if form.LatinTitle != "" {
     71 		if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinTitle) {
     72 			return release, errors.New("Latin title must not be empty or have leading/trailing whitespace")
     73 		}
     74 		if !domain.IsValidSortNameRegex.MatchString(form.LatinTitle) {
     75 			return release, errors.New("Latin title contains invalid characters")
     76 		}
     77 		latinTitle = &form.LatinTitle
     78 	}
     79 
     80 	releaseDate, err := time.Parse("2006-01-02", form.ReleaseDate)
     81 	if err != nil {
     82 		return release, fmt.Errorf("Invalid release date: %w", err)
     83 	}
     84 	minDate := time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)
     85 	maxDate := time.Now().AddDate(3, 0, 0)
     86 	if releaseDate.Before(minDate) || releaseDate.After(maxDate) {
     87 		return release, fmt.Errorf("Release date must be between %s and %s",
     88 			minDate.Format("2006-01-02"), maxDate.Format("2006-01-02"))
     89 	}
     90 
     91 	if len(form.SongID) != len(form.TrackNumber) {
     92 		return release, errors.New("Song ID and track number counts do not match")
     93 	}
     94 
     95 	songs := make([]domain.EditReleaseSong, 0, len(form.SongID))
     96 	seenTracknumbers := make(map[int]domain.SongID, len(form.SongID))
     97 	seenSongs := make(map[domain.SongID]int, len(form.SongID))
     98 
     99 	for i := 0; i < len(form.SongID); i++ {
    100 		songID := form.SongID[i]
    101 		trackNumber := form.TrackNumber[i]
    102 
    103 		if songID < 0 {
    104 			return release, errors.New("Invalid song selection")
    105 		}
    106 
    107 		if trackNumber < 1 {
    108 			return release, errors.New("Track number can not be less that 1")
    109 		}
    110 
    111 		if _, exists := seenTracknumbers[trackNumber]; exists {
    112 			return release, errors.New("Songs may not share the same track number")
    113 		}
    114 
    115 		if _, exists := seenSongs[songID]; exists {
    116 			return release, errors.New("A song cannot be added more than once")
    117 		}
    118 
    119 		seenTracknumbers[trackNumber] = songID
    120 		seenSongs[songID] = trackNumber
    121 
    122 		songs = append(songs, domain.EditReleaseSong{
    123 			SongID:      songID,
    124 			TrackNumber: trackNumber,
    125 		})
    126 	}
    127 
    128 	// musicbrainz release id
    129 	var musicbrainzReleaseID *domain.MusicBrainzID
    130 	if form.MusicBrainzReleaseID != "" {
    131 		if !domain.MusicBrainzIDIsValid(string(form.MusicBrainzReleaseID)) {
    132 			return release, fmt.Errorf("'%s' must be a valid MusicBrainz ID", form.MusicBrainzReleaseID)
    133 		}
    134 		musicbrainzReleaseID = &form.MusicBrainzReleaseID
    135 	}
    136 
    137 	release = domain.EditRelease{
    138 		MusicBrainzReleaseID: musicbrainzReleaseID,
    139 		Title:                form.Title,
    140 		LatinTitle:           latinTitle,
    141 		ReleaseDate:          releaseDate,
    142 		Artists:              artists,
    143 		Songs:                songs,
    144 		Type:                 form.Type,
    145 	}
    146 
    147 	return release, nil
    148 }
    149 
    150 func (ss *ServerState) EditRelease(w http.ResponseWriter, r *http.Request) {
    151 	logger := ss.logger.With("handler", "EditRelease")
    152 
    153 	idStr := r.PathValue("id")
    154 	id, err := strconv.Atoi(idStr)
    155 	if err != nil {
    156 		logger.Error("failed to convert release id to integer", "id", idStr, "err", err)
    157 		htmx.HxError(w, http.StatusBadRequest, err.Error())
    158 		return
    159 	}
    160 
    161 	slug := r.PathValue("slug")
    162 	logger = logger.With("id", id, "slug", slug)
    163 
    164 	switch r.Method {
    165 	case http.MethodGet:
    166 		release, err := db.Release(ss.db, domain.ReleaseID(id))
    167 		if err != nil {
    168 			logger.Error("failed to get release", "err", err)
    169 			htmx.HxError(w, http.StatusInternalServerError, fmt.Sprintf("failed to query for release '%s'", slug))
    170 			return
    171 		}
    172 
    173 		err = editrelease.EditRelease(editrelease.EditReleaseParams{
    174 			Release: release,
    175 		}).Render(r.Context(), w)
    176 		if err != nil {
    177 			if errors.Is(err, context.Canceled) {
    178 				return
    179 			}
    180 
    181 			logger.Error("failed to render edit release page", "err", err)
    182 			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    183 			return
    184 		}
    185 	case http.MethodPost:
    186 		err := r.ParseForm()
    187 		if err != nil {
    188 			logger.Error("failed to parse http form", "err", err)
    189 			htmx.HxError(w, http.StatusBadRequest, "Failed to edit release.")
    190 			return
    191 		}
    192 
    193 		var editReleaseForm EditReleaseForm
    194 		err = decoder.Decode(&editReleaseForm, r.PostForm)
    195 		if err != nil {
    196 			logger.Error("failed to decode http form", "err", err)
    197 			htmx.HxError(w, http.StatusBadRequest, err.Error())
    198 			return
    199 		}
    200 
    201 		updatedRelease, err := parseEditReleaseForm(editReleaseForm)
    202 		if err != nil {
    203 			logger.Error("failed to parse edited release form", "err", err)
    204 			htmx.HxError(w, http.StatusBadRequest, err.Error())
    205 			return
    206 		}
    207 		updatedRelease.ID = domain.ReleaseID(id)
    208 
    209 		tx, err := ss.db.BeginTx(r.Context(), nil)
    210 		if err != nil {
    211 			logger.Error("failed to begin db transaction", "err", err)
    212 			htmx.HxError(w, http.StatusInternalServerError, "Failed to edit release.")
    213 			return
    214 		}
    215 
    216 		err = db.UpdateRelease(tx, updatedRelease)
    217 		if err != nil {
    218 			if rollbackErr := tx.Rollback(); rollbackErr != nil {
    219 				logger.Error("failed to rollback db transaction", "err", rollbackErr)
    220 			}
    221 			logger.Error("failed to save edited release into db", "err", err)
    222 			htmx.HxError(w, http.StatusInternalServerError, "Failed to edit release.")
    223 			return
    224 		}
    225 
    226 		if err := tx.Commit(); err != nil {
    227 			logger.Error("failed to commit db transaction", "err", err)
    228 			htmx.HxError(w, http.StatusInternalServerError, "Failed to edit release.")
    229 			return
    230 		}
    231 
    232 		logger.Info("edited release", "release", updatedRelease)
    233 
    234 		release := domain.Release{
    235 			ID:         updatedRelease.ID,
    236 			Title:      updatedRelease.Title,
    237 			LatinTitle: updatedRelease.LatinTitle,
    238 		}
    239 
    240 		htmx.HxRedirect(w, http.StatusOK, routes.Release(release))
    241 	}
    242 }