feat: v0.0.5

This commit is contained in:
Gabe Farrell 2025-06-15 19:09:44 -04:00
parent 4c4ebc593d
commit 242a82ad8c
36 changed files with 694 additions and 174 deletions

View file

@ -3,6 +3,7 @@ package catalog
import (
"context"
"errors"
"fmt"
"slices"
"strings"
@ -17,11 +18,12 @@ import (
)
type AssociateArtistsOpts struct {
ArtistMbzIDs []uuid.UUID
ArtistNames []string
ArtistName string
TrackTitle string
Mbzc mbz.MusicBrainzCaller
ArtistMbzIDs []uuid.UUID
ArtistNames []string
ArtistMbidMap []ArtistMbidMap
ArtistName string
TrackTitle string
Mbzc mbz.MusicBrainzCaller
}
func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) ([]*models.Artist, error) {
@ -29,9 +31,19 @@ func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) (
var result []*models.Artist
if len(opts.ArtistMbzIDs) > 0 {
l.Debug().Msg("Associating artists by MusicBrainz ID(s)")
mbzMatches, err := matchArtistsByMBID(ctx, d, opts)
// use mbid map first, as it is the most reliable way to get mbid for artists
if len(opts.ArtistMbidMap) > 0 {
l.Debug().Msg("Associating artists by MusicBrainz ID(s) mappings")
mbzMatches, err := matchArtistsByMBIDMappings(ctx, d, opts)
if err != nil {
return nil, err
}
result = append(result, mbzMatches...)
}
if len(opts.ArtistMbzIDs) > len(result) {
l.Debug().Msg("Associating artists by list of MusicBrainz ID(s)")
mbzMatches, err := matchArtistsByMBID(ctx, d, opts, result)
if err != nil {
return nil, err
}
@ -60,11 +72,82 @@ func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) (
return result, nil
}
func matchArtistsByMBID(ctx context.Context, d db.DB, opts AssociateArtistsOpts) ([]*models.Artist, error) {
func matchArtistsByMBIDMappings(ctx context.Context, d db.DB, opts AssociateArtistsOpts) ([]*models.Artist, error) {
l := logger.FromContext(ctx)
var result []*models.Artist
for _, a := range opts.ArtistMbidMap {
// first, try to get by mbid
artist, err := d.GetArtist(ctx, db.GetArtistOpts{
MusicBrainzID: a.Mbid,
})
if err == nil {
l.Debug().Msgf("Artist '%s' found by MusicBrainz ID", artist.Name)
result = append(result, artist)
continue
}
if !errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("matchArtistsBYMBIDMappings: %w", err)
}
// then, try to get by mbz name
artist, err = d.GetArtist(ctx, db.GetArtistOpts{
Name: a.Artist,
})
if err == nil {
l.Debug().Msgf("Artist '%s' found by Name", a.Artist)
// ...associate with mbzid if found
err = d.UpdateArtist(ctx, db.UpdateArtistOpts{ID: artist.ID, MusicBrainzID: a.Mbid})
if err != nil {
l.Err(fmt.Errorf("matchArtistsBYMBIDMappings: %w", err)).Msgf("Failed to associate artist '%s' with MusicBrainz ID", artist.Name)
} else {
artist.MbzID = &a.Mbid
}
result = append(result, artist)
continue
}
if !errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("matchArtistsBYMBIDMappings: %w", err)
}
// then, try to get by aliases, or create
artist, err = resolveAliasOrCreateArtist(ctx, a.Mbid, opts.ArtistNames, d, opts.Mbzc)
if err != nil {
// if mbz unreachable, just create a new artist with provided name and mbid
l.Warn().Msg("MusicBrainz unreachable, creating new artist with provided MusicBrainz ID mapping")
var imgid uuid.UUID
imgUrl, err := images.GetArtistImage(ctx, images.ArtistImageOpts{
Aliases: []string{a.Artist},
})
if err == nil {
imgid = uuid.New()
err = DownloadAndCacheImage(ctx, imgid, imgUrl, ImageSourceSize())
if err != nil {
l.Err(fmt.Errorf("matchArtistsByMBIDMappings: %w", err)).Msgf("Failed to download artist image for artist '%s'", a.Artist)
imgid = uuid.Nil
}
} else {
l.Err(fmt.Errorf("matchArtistsByMBIDMappings: %w", err)).Msgf("Failed to get artist image for artist '%s'", a.Artist)
}
artist, err = d.SaveArtist(ctx, db.SaveArtistOpts{Name: a.Artist, MusicBrainzID: a.Mbid, Image: imgid, ImageSrc: imgUrl})
if err != nil {
l.Err(fmt.Errorf("matchArtistsByMBIDMappings: %w", err)).Msgf("Failed to create artist '%s' in database", a.Artist)
return nil, fmt.Errorf("matchArtistsByMBIDMappings: %w", err)
}
}
result = append(result, artist)
}
return result, nil
}
func matchArtistsByMBID(ctx context.Context, d db.DB, opts AssociateArtistsOpts, existing []*models.Artist) ([]*models.Artist, error) {
l := logger.FromContext(ctx)
var result []*models.Artist
for _, id := range opts.ArtistMbzIDs {
if artistExistsByMbzID(id, existing) || artistExistsByMbzID(id, result) {
l.Debug().Msgf("Artist with MusicBrainz ID %s already found, skipping...", id)
continue
}
if id == uuid.Nil {
l.Warn().Msg("Provided artist has uuid.Nil MusicBrainzID")
return matchArtistsByNames(ctx, opts.ArtistNames, result, d)
@ -229,3 +312,11 @@ func artistExists(name string, artists []*models.Artist) bool {
}
return false
}
func artistExistsByMbzID(id uuid.UUID, artists []*models.Artist) bool {
for _, a := range artists {
if a.MbzID != nil && *a.MbzID == id {
return true
}
}
return false
}

View file

@ -29,24 +29,30 @@ type SaveListenOpts struct {
Time time.Time
}
type ArtistMbidMap struct {
Artist string
Mbid uuid.UUID
}
type SubmitListenOpts struct {
// When true, skips registering the listen and only associates or creates the
// artist, release, release group, and track in DB
SkipSaveListen bool
MbzCaller mbz.MusicBrainzCaller
ArtistNames []string
Artist string
ArtistMbzIDs []uuid.UUID
TrackTitle string
RecordingMbzID uuid.UUID
Duration int32 // in seconds
ReleaseTitle string
ReleaseMbzID uuid.UUID
ReleaseGroupMbzID uuid.UUID
Time time.Time
UserID int32
Client string
MbzCaller mbz.MusicBrainzCaller
ArtistNames []string
Artist string
ArtistMbzIDs []uuid.UUID
ArtistMbidMappings []ArtistMbidMap
TrackTitle string
RecordingMbzID uuid.UUID
Duration int32 // in seconds
ReleaseTitle string
ReleaseMbzID uuid.UUID
ReleaseGroupMbzID uuid.UUID
Time time.Time
UserID int32
Client string
}
const (
@ -64,11 +70,12 @@ func SubmitListen(ctx context.Context, store db.DB, opts SubmitListenOpts) error
ctx,
store,
AssociateArtistsOpts{
ArtistMbzIDs: opts.ArtistMbzIDs,
ArtistNames: opts.ArtistNames,
ArtistName: opts.Artist,
Mbzc: opts.MbzCaller,
TrackTitle: opts.TrackTitle,
ArtistMbzIDs: opts.ArtistMbzIDs,
ArtistNames: opts.ArtistNames,
ArtistName: opts.Artist,
ArtistMbidMap: opts.ArtistMbidMappings,
Mbzc: opts.MbzCaller,
TrackTitle: opts.TrackTitle,
})
if err != nil {
l.Error().Err(err).Msg("Failed to associate artists to listen")

View file

@ -30,6 +30,15 @@ const (
ImageCacheDir = "image_cache"
)
func ImageSourceSize() (size ImageSize) {
if cfg.FullImageCacheEnabled() {
size = ImageSizeFull
} else {
size = ImageSizeLarge
}
return
}
func ParseImageSize(size string) (ImageSize, error) {
switch strings.ToLower(size) {
case "small":

View file

@ -856,3 +856,64 @@ func TestSubmitListen_MusicBrainzUnreachable(t *testing.T) {
require.NoError(t, err)
assert.True(t, exists, "expected listen row to exist")
}
func TestSubmitListen_MusicBrainzUnreachableMBIDMappings(t *testing.T) {
truncateTestData(t)
// correctly associate MBID when musicbrainz unreachable, but map provided
ctx := context.Background()
mbzc := &mbz.MbzErrorCaller{}
artistMbzID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
artist2MbzID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
releaseGroupMbzID := uuid.MustParse("00000000-0000-0000-0000-000000000011")
releaseMbzID := uuid.MustParse("00000000-0000-0000-0000-000000000101")
trackMbzID := uuid.MustParse("00000000-0000-0000-0000-000000001001")
artistMbzIdMap := []catalog.ArtistMbidMap{{Artist: "ATARASHII GAKKO!", Mbid: artistMbzID}, {Artist: "Featured Artist", Mbid: artist2MbzID}}
opts := catalog.SubmitListenOpts{
MbzCaller: mbzc,
ArtistNames: []string{"ATARASHII GAKKO!", "Featured Artist"},
Artist: "ATARASHII GAKKO! feat. Featured Artist",
ArtistMbzIDs: []uuid.UUID{
artistMbzID,
},
TrackTitle: "Tokyo Calling",
RecordingMbzID: trackMbzID,
ReleaseTitle: "AG! Calling",
ReleaseMbzID: releaseMbzID,
ReleaseGroupMbzID: releaseGroupMbzID,
ArtistMbidMappings: artistMbzIdMap,
Time: time.Now(),
UserID: 1,
}
err := catalog.SubmitListen(ctx, store, opts)
require.NoError(t, err)
// Verify that the listen was saved
exists, err := store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM listens
WHERE track_id = $1
)`, 1)
require.NoError(t, err)
assert.True(t, exists, "expected listen row to exist")
// Verify that the artist has the mbid saved
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artists
WHERE musicbrainz_id = $1
)`, artistMbzID)
require.NoError(t, err)
assert.True(t, exists, "expected artist to have correct musicbrainz id")
// Verify that the artist has the mbid saved
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artists
WHERE musicbrainz_id = $1
)`, artist2MbzID)
require.NoError(t, err)
assert.True(t, exists, "expected artist to have correct musicbrainz id")
}