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 }