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:
@@ -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 {
|
func (r *noopTrackRepo) Save(ctx context.Context, track *domain.Track) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *noopTrackRepo) SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
protoc-gen-go-grpc
|
protoc-gen-go-grpc
|
||||||
|
|
||||||
grpcurl
|
grpcurl
|
||||||
|
|
||||||
|
opencode
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
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`
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer extRows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for extRows.Next() {
|
||||||
var ext domain.ExternalID
|
var ext domain.ExternalID
|
||||||
var url *string
|
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
|
return err
|
||||||
}
|
}
|
||||||
ext.URL = derefString(url)
|
ext.URL = derefString(url)
|
||||||
album.ExternalIDs = append(album.ExternalIDs, ext)
|
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)
|
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) {
|
func (r *TrackRepository) scanTrack(ctx context.Context, query string, args ...any) (*domain.Track, error) {
|
||||||
row := r.pool.QueryRow(ctx, query, args...)
|
row := r.pool.QueryRow(ctx, query, args...)
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ type TrackRepository interface {
|
|||||||
GetByISRC(ctx context.Context, isrc string) (*domain.Track, error)
|
GetByISRC(ctx context.Context, isrc string) (*domain.Track, error)
|
||||||
GetByAlbumID(ctx context.Context, albumID string) ([]domain.Track, error)
|
GetByAlbumID(ctx context.Context, albumID string) ([]domain.Track, error)
|
||||||
Save(ctx context.Context, track *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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.ensureArtistsCached(ctx, []domain.Album{*album})
|
||||||
|
|
||||||
if saveErr := s.albums.Save(ctx, album); saveErr != nil {
|
if saveErr := s.albums.Save(ctx, album); saveErr != nil {
|
||||||
log.Warn().Err(saveErr).Str("album_id", id).Msg("failed to cache album")
|
log.Warn().Err(saveErr).Str("album_id", id).Msg("failed to cache album")
|
||||||
return album, nil
|
return album, nil
|
||||||
@@ -246,5 +248,20 @@ func (s *MetadataService) GetAlbumTracks(ctx context.Context, albumID string) ([
|
|||||||
|
|
||||||
metrics.CacheMisses.WithLabelValues("album_tracks").Inc()
|
metrics.CacheMisses.WithLabelValues("album_tracks").Inc()
|
||||||
log.Debug().Str("album_id", albumID).Str("provider", s.provider.Name()).Msg("album tracks cache miss, querying provider")
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
func (r *noopTrackRepo) Save(ctx context.Context, track *domain.Track) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *noopTrackRepo) SaveAlbumTracks(ctx context.Context, albumID string, tracks []domain.Track) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user