diff --git a/cmd/server/noop_repo.go b/cmd/server/noop_repo.go index b78cf47..da51cb8 100644 --- a/cmd/server/noop_repo.go +++ b/cmd/server/noop_repo.go @@ -72,3 +72,7 @@ func (r *noopTrackRepo) GetByAlbumID(ctx context.Context, albumID string) ([]dom func (r *noopTrackRepo) Save(ctx context.Context, track *domain.Track) error { return nil } + +func (r *noopTrackRepo) SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error { + return nil +} diff --git a/flake.nix b/flake.nix index 461c578..8114558 100644 --- a/flake.nix +++ b/flake.nix @@ -81,6 +81,8 @@ protoc-gen-go-grpc grpcurl + + opencode ]; }; }; diff --git a/internal/repository/postgres/album.go b/internal/repository/postgres/album.go index 110bed6..a3f7225 100644 --- a/internal/repository/postgres/album.go +++ b/internal/repository/postgres/album.go @@ -319,21 +319,57 @@ func (r *AlbumRepository) scanAlbumRow(row pgx.Row) (*domain.Album, error) { func (r *AlbumRepository) loadRelations(ctx context.Context, album *domain.Album) error { extQuery := `SELECT source, source_id, url FROM album_external_ids WHERE album_id = $1` - rows, err := r.pool.Query(ctx, extQuery, album.ID) + extRows, err := r.pool.Query(ctx, extQuery, album.ID) if err != nil { return err } - defer rows.Close() + defer extRows.Close() - for rows.Next() { + for extRows.Next() { var ext domain.ExternalID var url *string - if err := rows.Scan(&ext.Source, &ext.SourceID, &url); err != nil { + if err := extRows.Scan(&ext.Source, &ext.SourceID, &url); err != nil { return err } ext.URL = derefString(url) album.ExternalIDs = append(album.ExternalIDs, ext) } + if err := extRows.Err(); err != nil { + return err + } - return rows.Err() + artistQuery := ` + SELECT a.id, a.name, a.sort_name, a.artist_type, a.country, + aa.role, aa.position + FROM album_artists aa + JOIN artists a ON a.id = aa.artist_id + WHERE aa.album_id = $1 + ORDER BY aa.position` + + artistRows, err := r.pool.Query(ctx, artistQuery, album.ID) + if err != nil { + return err + } + defer artistRows.Close() + + for artistRows.Next() { + var ( + ac domain.ArtistCredit + sortName *string + artType *string + country *string + ) + if err := artistRows.Scan( + &ac.Artist.ID, &ac.Artist.Name, &sortName, &artType, &country, + &ac.Role, &ac.Position, + ); err != nil { + return err + } + ac.Artist.SortName = derefString(sortName) + ac.Artist.Type = derefString(artType) + ac.Artist.Country = derefString(country) + album.Artists = append(album.Artists, ac) + } + + return artistRows.Err() } diff --git a/internal/repository/postgres/track.go b/internal/repository/postgres/track.go index c93c9f8..330f40d 100644 --- a/internal/repository/postgres/track.go +++ b/internal/repository/postgres/track.go @@ -170,6 +170,67 @@ func (r *TrackRepository) Save(ctx context.Context, track *domain.Track) error { return tx.Commit(ctx) } +func (r *TrackRepository) SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + for i := range tracks { + track := &tracks[i] + + var source, sourceID string + if len(track.ExternalIDs) > 0 { + source = track.ExternalIDs[0].Source + sourceID = track.ExternalIDs[0].SourceID + } + + trackQuery := ` + INSERT INTO tracks (id, title, duration_ms, isrc, explicit, source, source_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + duration_ms = EXCLUDED.duration_ms, + isrc = EXCLUDED.isrc, + explicit = EXCLUDED.explicit, + updated_at = now()` + + _, err = tx.Exec(ctx, trackQuery, + track.ID, track.Title, track.DurationMs, nullString(track.ISRC), + track.Explicit, source, sourceID) + if err != nil { + return err + } + + for _, ext := range track.ExternalIDs { + extQuery := ` + INSERT INTO track_external_ids (track_id, source, source_id, url) + VALUES ($1, $2, $3, $4) + ON CONFLICT (track_id, source, source_id) DO UPDATE SET + url = EXCLUDED.url, + fetched_at = now()` + + _, err = tx.Exec(ctx, extQuery, track.ID, ext.Source, ext.SourceID, nullString(ext.URL)) + if err != nil { + return err + } + } + + linkQuery := ` + INSERT INTO album_tracks (album_id, track_id, disc_number, track_number) + VALUES ($1, $2, $3, $4) + ON CONFLICT (album_id, track_id) DO NOTHING` + + _, err = tx.Exec(ctx, linkQuery, albumID, track.ID, track.DiscNumber, track.TrackNumber) + if err != nil { + return err + } + } + + return tx.Commit(ctx) +} + func (r *TrackRepository) scanTrack(ctx context.Context, query string, args ...any) (*domain.Track, error) { row := r.pool.QueryRow(ctx, query, args...) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 884f1d8..2c983a6 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -28,4 +28,5 @@ type TrackRepository interface { GetByISRC(ctx context.Context, isrc string) (*domain.Track, error) GetByAlbumID(ctx context.Context, albumID string) ([]domain.Track, error) Save(ctx context.Context, track *domain.Track) error + SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error } diff --git a/internal/service/metadata.go b/internal/service/metadata.go index b79fd5d..c2e32c4 100644 --- a/internal/service/metadata.go +++ b/internal/service/metadata.go @@ -108,6 +108,8 @@ func (s *MetadataService) GetAlbum(ctx context.Context, id string) (*domain.Albu return nil, err } + s.ensureArtistsCached(ctx, []domain.Album{*album}) + if saveErr := s.albums.Save(ctx, album); saveErr != nil { log.Warn().Err(saveErr).Str("album_id", id).Msg("failed to cache album") return album, nil @@ -246,5 +248,20 @@ func (s *MetadataService) GetAlbumTracks(ctx context.Context, albumID string) ([ metrics.CacheMisses.WithLabelValues("album_tracks").Inc() log.Debug().Str("album_id", albumID).Str("provider", s.provider.Name()).Msg("album tracks cache miss, querying provider") - return s.provider.GetAlbumTracks(ctx, albumID) + + tracks, err = s.provider.GetAlbumTracks(ctx, albumID) + if err != nil { + return nil, err + } + + album, err := s.albums.GetByExternalID(ctx, s.provider.Name(), albumID) + if err == nil { + if saveErr := s.tracks.SaveAlbumTracks(ctx, album.ID, tracks); saveErr != nil { + log.Warn().Err(saveErr).Str("album_id", albumID).Msg("failed to cache album tracks") + } else { + log.Debug().Str("album_id", albumID).Int("count", len(tracks)).Msg("album tracks cached") + } + } + + return tracks, nil } diff --git a/tests/e2e/noop_repo_test.go b/tests/e2e/noop_repo_test.go index ca36423..37eb4e7 100644 --- a/tests/e2e/noop_repo_test.go +++ b/tests/e2e/noop_repo_test.go @@ -72,3 +72,7 @@ func (r *noopTrackRepo) GetByAlbumID(ctx context.Context, albumID string) ([]dom func (r *noopTrackRepo) Save(ctx context.Context, track *domain.Track) error { return nil } + +func (r *noopTrackRepo) SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error { + return nil +}