lyrics

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

release.go (10673B)


      1 package db
      2 
      3 import (
      4 	"fmt"
      5 	"lyrics/internal/domain"
      6 	"strings"
      7 	"time"
      8 )
      9 
     10 type releaseArtistRow struct {
     11 	artistID  domain.ArtistID
     12 	role      domain.ArtistRole
     13 	sortOrder int
     14 }
     15 
     16 type releaseSongRow struct {
     17 	songID      domain.SongID
     18 	trackNumber int
     19 }
     20 
     21 func Release(e Execer, id domain.ReleaseID) (domain.Release, error) {
     22 	var release domain.Release
     23 	var releaseDateStr string
     24 
     25 	err := e.QueryRow(`
     26 		select id, musicbrainz_release_id, title, latin_title, release_date, type
     27 		from releases
     28 		where id = ?
     29 	`, id).Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type)
     30 	if err != nil {
     31 		return domain.Release{}, fmt.Errorf("failed to get release: %w", err)
     32 	}
     33 
     34 	release.ReleaseDate, err = time.Parse(time.RFC3339, releaseDateStr)
     35 	if err != nil {
     36 		return domain.Release{}, fmt.Errorf("failed to parse release release_date: %w", err)
     37 	}
     38 
     39 	// fetch release_artists rows
     40 	var artistRows []releaseArtistRow
     41 
     42 	{
     43 		rows, err := e.Query(`
     44 			select artist_id, role, sort_order
     45 			from release_artists
     46 			where release_id = ?
     47 			order by sort_order
     48 		`, id)
     49 		if err != nil {
     50 			return domain.Release{}, fmt.Errorf("failed to query release artists: %w", err)
     51 		}
     52 
     53 		for rows.Next() {
     54 			var r releaseArtistRow
     55 			if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil {
     56 				_ = rows.Close()
     57 				return domain.Release{}, fmt.Errorf("failed to scan release artist: %w", err)
     58 			}
     59 			artistRows = append(artistRows, r)
     60 		}
     61 		if err := rows.Err(); err != nil {
     62 			_ = rows.Close()
     63 			return domain.Release{}, fmt.Errorf("failed to iterate release artists: %w", err)
     64 		}
     65 		_ = rows.Close()
     66 	}
     67 
     68 	// fetch full artist records in one query
     69 	if len(artistRows) > 0 {
     70 		artistIDs := make([]any, len(artistRows))
     71 		for i, r := range artistRows {
     72 			artistIDs[i] = r.artistID
     73 		}
     74 		placeholders := strings.Repeat("?,", len(artistIDs))
     75 		placeholders = placeholders[:len(placeholders)-1]
     76 
     77 		artistMap := make(map[domain.ArtistID]domain.Artist, len(artistIDs))
     78 		{
     79 			rows, err := e.Query(`
     80 				select id, musicbrainz_artist_id, name, latin_name, disambiguation, bio
     81 				from artists
     82 				where id in (`+placeholders+`)
     83 			`, artistIDs...)
     84 			if err != nil {
     85 				return domain.Release{}, fmt.Errorf("failed to query artists: %w", err)
     86 			}
     87 
     88 			for rows.Next() {
     89 				var a domain.Artist
     90 				if err := rows.Scan(&a.ID, &a.MusicBrainzArtistID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil {
     91 					_ = rows.Close()
     92 					return domain.Release{}, fmt.Errorf("failed to scan artist: %w", err)
     93 				}
     94 				artistMap[a.ID] = a
     95 			}
     96 			if err := rows.Err(); err != nil {
     97 				_ = rows.Close()
     98 				return domain.Release{}, fmt.Errorf("failed to iterate artists: %w", err)
     99 			}
    100 			_ = rows.Close()
    101 		}
    102 
    103 		release.Artists = make([]domain.ReleaseArtist, len(artistRows))
    104 		for i, r := range artistRows {
    105 			release.Artists[i] = domain.ReleaseArtist{
    106 				Artist:    artistMap[r.artistID],
    107 				Role:      r.role,
    108 				SortOrder: r.sortOrder,
    109 			}
    110 		}
    111 	}
    112 
    113 	// fetch songs on this release ordered by track_number
    114 	var songRows []releaseSongRow
    115 	{
    116 		rows, err := e.Query(`
    117 			select song_id, track_number
    118 			from song_releases
    119 			where release_id = ?
    120 			order by track_number
    121 		`, id)
    122 		if err != nil {
    123 			return domain.Release{}, fmt.Errorf("failed to query release songs: %w", err)
    124 		}
    125 
    126 		for rows.Next() {
    127 			var r releaseSongRow
    128 			if err := rows.Scan(&r.songID, &r.trackNumber); err != nil {
    129 				_ = rows.Close()
    130 				return domain.Release{}, fmt.Errorf("failed to scan release song: %w", err)
    131 			}
    132 			songRows = append(songRows, r)
    133 		}
    134 		if err := rows.Err(); err != nil {
    135 			_ = rows.Close()
    136 			return domain.Release{}, fmt.Errorf("failed to iterate release songs: %w", err)
    137 		}
    138 		_ = rows.Close()
    139 	}
    140 
    141 	release.Songs = make([]domain.ReleaseSong, 0, len(songRows))
    142 	for _, r := range songRows {
    143 		song, err := Song(e, r.songID)
    144 		if err != nil {
    145 			return domain.Release{}, fmt.Errorf("failed to get song %d: %w", r.songID, err)
    146 		}
    147 		release.Songs = append(release.Songs, domain.ReleaseSong{
    148 			Song:        song,
    149 			TrackNumber: r.trackNumber,
    150 		})
    151 	}
    152 
    153 	return release, nil
    154 }
    155 
    156 func UpdateRelease(e Execer, release domain.EditRelease) error {
    157 	result, err := e.Exec(`
    158 		update releases 
    159 		set
    160 			type = ?,
    161 			musicbrainz_release_id = ?,
    162 			title = ?,
    163 			latin_title = ?,
    164 			release_date = ?
    165 		where id = ?
    166 	`, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), release.ID)
    167 	if err != nil {
    168 		return fmt.Errorf("failed to update release: %w", err)
    169 	}
    170 
    171 	rowsAffected, err := result.RowsAffected()
    172 	if err != nil {
    173 		return fmt.Errorf("failed to check updated release rows: %w", err)
    174 	}
    175 	if rowsAffected == 0 {
    176 		return fmt.Errorf("release %d does not exist", release.ID)
    177 	}
    178 
    179 	_, err = e.Exec(`
    180 		delete from release_artists
    181 		where release_id = ?
    182 	`, release.ID)
    183 	if err != nil {
    184 		return fmt.Errorf("failed to delete release primary artists: %w", err)
    185 	}
    186 
    187 	for _, artist := range release.Artists {
    188 		_, err = e.Exec(`
    189 			insert into release_artists (
    190 				release_id,
    191 				artist_id,
    192 				role,
    193 				sort_order
    194 			)
    195 			values (?, ?, ?, ?)
    196 		`, release.ID, artist.ArtistID, artist.Role, artist.SortOrder)
    197 		if err != nil {
    198 			return fmt.Errorf("failed to insert release artist: %w", err)
    199 		}
    200 	}
    201 
    202 	_, err = e.Exec(`
    203 		delete from song_releases
    204 		where release_id = ?
    205 	`, release.ID)
    206 	if err != nil {
    207 		return fmt.Errorf("failed to delete release songs: %w", err)
    208 	}
    209 
    210 	for _, song := range release.Songs {
    211 		_, err = e.Exec(`
    212 			insert into song_releases (
    213 				song_id,
    214 				release_id,
    215 				track_number
    216 			)
    217 			values (?, ?, ?)
    218 		`, song.SongID, release.ID, song.TrackNumber)
    219 		if err != nil {
    220 			return fmt.Errorf("failed to update release song: %w", err)
    221 		}
    222 	}
    223 
    224 	return nil
    225 }
    226 
    227 func InsertRelease(e Execer, release domain.NewRelease) (domain.ReleaseID, error) {
    228 	result, err := e.Exec(`
    229 		insert into releases (
    230 			type,
    231 			musicbrainz_release_id,
    232 			title,
    233 			latin_title,
    234 			release_date,
    235 			created_at
    236 		)
    237 		values (?, ?, ?, ?, ?, ?)
    238 	`, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339))
    239 	if err != nil {
    240 		return 0, fmt.Errorf("failed to insert release: %w", err)
    241 	}
    242 
    243 	lastID, err := result.LastInsertId()
    244 	if err != nil {
    245 		return 0, fmt.Errorf("failed to retrieve last inserted id: %w", err)
    246 	}
    247 
    248 	for _, artist := range release.Artists {
    249 		_, err = e.Exec(`
    250 			insert into release_artists (
    251 				release_id,
    252 				artist_id,
    253 				role,
    254 				sort_order
    255 			)
    256 			values (?, ?, ?, ?)
    257 			on conflict(release_id, artist_id, role) do update set
    258 				sort_order = excluded.sort_order
    259 		`, lastID, artist.ArtistID, artist.Role, artist.SortOrder)
    260 		if err != nil {
    261 			return 0, fmt.Errorf("failed to insert release artist: %w", err)
    262 		}
    263 	}
    264 
    265 	return domain.ReleaseID(lastID), nil
    266 }
    267 
    268 func SearchRelease(e Execer, releaseName string, limit, offset int64) (int, []domain.Release, error) {
    269 	releaseName = strings.TrimSpace(releaseName)
    270 	if releaseName == "" {
    271 		return 0, nil, nil
    272 	}
    273 
    274 	wildcard := "%" + releaseName + "%"
    275 
    276 	var totalRows int
    277 	err := e.QueryRow(`
    278 		select count(*)
    279 		from releases
    280 		where title like ? collate nocase
    281 		   or latin_title like ? collate nocase
    282 		order by title
    283 	`, wildcard, wildcard).Scan(&totalRows)
    284 	if err != nil {
    285 		return 0, nil, fmt.Errorf("failed to count total releases: %w", err)
    286 	}
    287 
    288 	rows, err := e.Query(`
    289 		select id, musicbrainz_release_id, title, latin_title, release_date, type
    290 		from releases
    291 		where title like ? collate nocase
    292 		   or latin_title like ? collate nocase
    293 		order by title
    294 		limit ? offset ?
    295 	`, wildcard, wildcard, limit, offset)
    296 	if err != nil {
    297 		return 0, nil, fmt.Errorf("failed to query releases: %w", err)
    298 	}
    299 	defer func() {
    300 		_ = rows.Close()
    301 	}()
    302 
    303 	var results []domain.Release
    304 	for rows.Next() {
    305 		var release domain.Release
    306 		var releaseDateStr string
    307 
    308 		if err := rows.Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type); err != nil {
    309 			return 0, nil, fmt.Errorf("failed to scan release row: %w", err)
    310 		}
    311 
    312 		releaseDate, err := time.Parse(time.RFC3339, releaseDateStr)
    313 		if err != nil {
    314 			return 0, nil, fmt.Errorf("failed to parse release date string '%s': %w", releaseDateStr, err)
    315 		}
    316 
    317 		release.ReleaseDate = releaseDate
    318 
    319 		// fetch release_artists rows
    320 		var artistRows []releaseArtistRow
    321 
    322 		{
    323 			rows, err := e.Query(`
    324 				select artist_id, role, sort_order
    325 				from release_artists
    326 				where release_id = ?
    327 				order by sort_order
    328 			`, release.ID)
    329 			if err != nil {
    330 				return 0, []domain.Release{}, fmt.Errorf("failed to query release artists: %w", err)
    331 			}
    332 
    333 			for rows.Next() {
    334 				var r releaseArtistRow
    335 				if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil {
    336 					_ = rows.Close()
    337 					return 0, []domain.Release{}, fmt.Errorf("failed to scan release artist: %w", err)
    338 				}
    339 				artistRows = append(artistRows, r)
    340 			}
    341 			if err := rows.Err(); err != nil {
    342 				_ = rows.Close()
    343 				return 0, []domain.Release{}, fmt.Errorf("failed to iterate release artists: %w", err)
    344 			}
    345 			_ = rows.Close()
    346 		}
    347 
    348 		// fetch full artist records in one query
    349 		if len(artistRows) > 0 {
    350 			artistIDs := make([]any, len(artistRows))
    351 			for i, r := range artistRows {
    352 				artistIDs[i] = r.artistID
    353 			}
    354 			placeholders := strings.Repeat("?,", len(artistIDs))
    355 			placeholders = placeholders[:len(placeholders)-1]
    356 
    357 			artistMap := make(map[domain.ArtistID]domain.Artist, len(artistIDs))
    358 			{
    359 				rows, err := e.Query(`
    360 					select id, musicbrainz_artist_id, name, latin_name, disambiguation, bio
    361 					from artists
    362 					where id in (`+placeholders+`)
    363 				`, artistIDs...)
    364 				if err != nil {
    365 					return 0, []domain.Release{}, fmt.Errorf("failed to query artists: %w", err)
    366 				}
    367 
    368 				for rows.Next() {
    369 					var a domain.Artist
    370 					if err := rows.Scan(&a.ID, &a.MusicBrainzArtistID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil {
    371 						_ = rows.Close()
    372 						return 0, []domain.Release{}, fmt.Errorf("failed to scan artist: %w", err)
    373 					}
    374 					artistMap[a.ID] = a
    375 				}
    376 				if err := rows.Err(); err != nil {
    377 					_ = rows.Close()
    378 					return 0, []domain.Release{}, fmt.Errorf("failed to iterate artists: %w", err)
    379 				}
    380 				_ = rows.Close()
    381 			}
    382 
    383 			release.Artists = make([]domain.ReleaseArtist, len(artistRows))
    384 			for i, r := range artistRows {
    385 				release.Artists[i] = domain.ReleaseArtist{
    386 					Artist:    artistMap[r.artistID],
    387 					Role:      r.role,
    388 					SortOrder: r.sortOrder,
    389 				}
    390 			}
    391 		}
    392 
    393 		results = append(results, release)
    394 	}
    395 
    396 	return totalRows, results, rows.Err()
    397 }