feat: cache album tracks and load album artists on album fetch
- Add SaveAlbumTracks to TrackRepository for batch track caching - Extend album loadRelations to join album_artists with artist data - Cache album tracks in GetAlbumTracks after provider fetch - Ensure artists cached when fetching single album
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user