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:
Alexander
2026-05-09 19:28:44 +02:00
parent 9f1318e79e
commit 88372c9301
7 changed files with 131 additions and 6 deletions
+41 -5
View File
@@ -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()
}
+61
View File
@@ -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...)
+1
View File
@@ -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
}
+18 -1
View File
@@ -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
}