mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
feat: v0.0.8
This commit is contained in:
parent
00e7782be2
commit
80b6f4deaa
66 changed files with 1559 additions and 916 deletions
|
|
@ -3,6 +3,7 @@ package catalog
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/gabehf/koito/internal/cfg"
|
||||
|
|
@ -23,12 +24,13 @@ type AssociateAlbumOpts struct {
|
|||
ReleaseName string
|
||||
TrackName string // required
|
||||
Mbzc mbz.MusicBrainzCaller
|
||||
SkipCacheImage bool
|
||||
}
|
||||
|
||||
func AssociateAlbum(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*models.Album, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
if opts.TrackName == "" {
|
||||
return nil, errors.New("required parameter TrackName missing")
|
||||
return nil, errors.New("AssociateAlbum: required parameter TrackName missing")
|
||||
}
|
||||
releaseTitle := opts.ReleaseName
|
||||
if releaseTitle == "" {
|
||||
|
|
@ -56,7 +58,7 @@ func matchAlbumByMbzReleaseID(ctx context.Context, d db.DB, opts AssociateAlbumO
|
|||
Image: a.Image,
|
||||
}, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchAlbumByMbzReleaseID: %w", err)
|
||||
} else {
|
||||
l.Debug().Msgf("Album '%s' could not be found by MusicBrainz Release ID", opts.ReleaseName)
|
||||
rg, err := createOrUpdateAlbumWithMbzReleaseID(ctx, d, opts)
|
||||
|
|
@ -69,14 +71,17 @@ func matchAlbumByMbzReleaseID(ctx context.Context, d db.DB, opts AssociateAlbumO
|
|||
|
||||
func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*models.Album, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
|
||||
release, err := opts.Mbzc.GetRelease(ctx, opts.ReleaseMbzID)
|
||||
if err != nil {
|
||||
l.Warn().Msg("MusicBrainz unreachable, falling back to release title matching")
|
||||
l.Warn().Msg("createOrUpdateAlbumWithMbzReleaseID: MusicBrainz unreachable, falling back to release title matching")
|
||||
return matchAlbumByTitle(ctx, d, opts)
|
||||
}
|
||||
|
||||
var album *models.Album
|
||||
titles := []string{release.Title, opts.ReleaseName}
|
||||
utils.Unique(&titles)
|
||||
|
||||
l.Debug().Msgf("Searching for albums '%v' from artist id %d in DB", titles, opts.Artists[0].ID)
|
||||
album, err = d.GetAlbum(ctx, db.GetAlbumOpts{
|
||||
ArtistID: opts.Artists[0].ID,
|
||||
|
|
@ -89,27 +94,29 @@ func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts Asso
|
|||
MusicBrainzID: opts.ReleaseMbzID,
|
||||
})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to update album with MusicBrainz Release ID")
|
||||
return nil, err
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to update album with MusicBrainz Release ID")
|
||||
return nil, fmt.Errorf("createOrUpdateAlbumWithMbzReleaseID: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Updated album '%s' with MusicBrainz Release ID", album.Title)
|
||||
|
||||
if opts.ReleaseGroupMbzID != uuid.Nil {
|
||||
aliases, err := opts.Mbzc.GetReleaseTitles(ctx, opts.ReleaseGroupMbzID)
|
||||
if err == nil {
|
||||
l.Debug().Msgf("Associating aliases '%s' with Release '%s'", aliases, album.Title)
|
||||
err = d.SaveAlbumAliases(ctx, album.ID, aliases, "MusicBrainz")
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to save aliases")
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to save aliases")
|
||||
}
|
||||
} else {
|
||||
l.Info().AnErr("err", err).Msg("Failed to get release group from MusicBrainz")
|
||||
l.Info().AnErr("err", err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to get release group from MusicBrainz")
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
l.Err(err).Msg("Error while searching for album by MusicBrainz Release ID")
|
||||
return nil, err
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: error while searching for album by MusicBrainz Release ID")
|
||||
return nil, fmt.Errorf("createOrUpdateAlbumWithMbzReleaseID: %w", err)
|
||||
} else {
|
||||
l.Debug().Msgf("Album %s could not be found. Creating...", release.Title)
|
||||
|
||||
var variousArtists bool
|
||||
for _, artistCredit := range release.ArtistCredit {
|
||||
if artistCredit.Name == "Various Artists" {
|
||||
|
|
@ -117,6 +124,7 @@ func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts Asso
|
|||
variousArtists = true
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug().Msg("Searching for album images...")
|
||||
var imgid uuid.UUID
|
||||
imgUrl, err := images.GetAlbumImage(ctx, images.AlbumImageOpts{
|
||||
|
|
@ -124,23 +132,28 @@ func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts Asso
|
|||
Album: release.Title,
|
||||
ReleaseMbzID: &opts.ReleaseMbzID,
|
||||
})
|
||||
|
||||
if err == nil && imgUrl != "" {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
imgid = uuid.New()
|
||||
l.Debug().Msg("Downloading album image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
if !opts.SkipCacheImage {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
l.Debug().Msg("Downloading album image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to cache image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
l.Debug().Msgf("Failed to get album images for %s: %s", release.Title, err.Error())
|
||||
l.Debug().Msgf("createOrUpdateAlbumWithMbzReleaseID: failed to get album images for %s: %s", release.Title, err.Error())
|
||||
}
|
||||
|
||||
album, err = d.SaveAlbum(ctx, db.SaveAlbumOpts{
|
||||
Title: release.Title,
|
||||
MusicBrainzID: opts.ReleaseMbzID,
|
||||
|
|
@ -150,22 +163,25 @@ func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts Asso
|
|||
ImageSrc: imgUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("createOrUpdateAlbumWithMbzReleaseID: %w", err)
|
||||
}
|
||||
|
||||
if opts.ReleaseGroupMbzID != uuid.Nil {
|
||||
aliases, err := opts.Mbzc.GetReleaseTitles(ctx, opts.ReleaseGroupMbzID)
|
||||
if err == nil {
|
||||
l.Debug().Msgf("Associating aliases '%s' with Release '%s'", aliases, album.Title)
|
||||
err = d.SaveAlbumAliases(ctx, album.ID, aliases, "MusicBrainz")
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to save aliases")
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to save aliases")
|
||||
}
|
||||
} else {
|
||||
l.Info().AnErr("err", err).Msg("Failed to get release group from MusicBrainz")
|
||||
l.Info().AnErr("err", err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to get release group from MusicBrainz")
|
||||
}
|
||||
}
|
||||
|
||||
l.Info().Msgf("Created album '%s' with MusicBrainz Release ID", album.Title)
|
||||
}
|
||||
|
||||
return &models.Album{
|
||||
ID: album.ID,
|
||||
MbzID: &opts.ReleaseMbzID,
|
||||
|
|
@ -176,12 +192,14 @@ func createOrUpdateAlbumWithMbzReleaseID(ctx context.Context, d db.DB, opts Asso
|
|||
|
||||
func matchAlbumByTitle(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*models.Album, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
|
||||
var releaseName string
|
||||
if opts.ReleaseName != "" {
|
||||
releaseName = opts.ReleaseName
|
||||
} else {
|
||||
releaseName = opts.TrackName
|
||||
}
|
||||
|
||||
a, err := d.GetAlbum(ctx, db.GetAlbumOpts{
|
||||
Title: releaseName,
|
||||
ArtistID: opts.Artists[0].ID,
|
||||
|
|
@ -195,11 +213,11 @@ func matchAlbumByTitle(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*
|
|||
MusicBrainzID: opts.ReleaseMbzID,
|
||||
})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to associate existing release with MusicBrainz ID")
|
||||
l.Err(err).Msg("matchAlbumByTitle: failed to associate existing release with MusicBrainz ID")
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchAlbumByTitle: %w", err)
|
||||
} else {
|
||||
var imgid uuid.UUID
|
||||
imgUrl, err := images.GetAlbumImage(ctx, images.AlbumImageOpts{
|
||||
|
|
@ -208,22 +226,25 @@ func matchAlbumByTitle(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*
|
|||
ReleaseMbzID: &opts.ReleaseMbzID,
|
||||
})
|
||||
if err == nil && imgUrl != "" {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
imgid = uuid.New()
|
||||
l.Debug().Msg("Downloading album image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
if !opts.SkipCacheImage {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
l.Debug().Msg("Downloading album image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("createOrUpdateAlbumWithMbzReleaseID: failed to cache image")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
l.Debug().Msgf("Failed to get album images for %s: %s", opts.ReleaseName, err.Error())
|
||||
l.Debug().AnErr("error", err).Msgf("matchAlbumByTitle: failed to get album images for %s", opts.ReleaseName)
|
||||
}
|
||||
|
||||
a, err = d.SaveAlbum(ctx, db.SaveAlbumOpts{
|
||||
Title: releaseName,
|
||||
ArtistIDs: utils.FlattenArtistIDs(opts.Artists),
|
||||
|
|
@ -232,10 +253,11 @@ func matchAlbumByTitle(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*
|
|||
ImageSrc: imgUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchAlbumByTitle: %w", err)
|
||||
}
|
||||
l.Info().Msgf("Created album '%s' with artist and title", a.Title)
|
||||
}
|
||||
|
||||
return &models.Album{
|
||||
ID: a.ID,
|
||||
Title: a.Title,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ type AssociateArtistsOpts struct {
|
|||
ArtistName string
|
||||
TrackTitle string
|
||||
Mbzc mbz.MusicBrainzCaller
|
||||
|
||||
SkipCacheImage bool
|
||||
}
|
||||
|
||||
func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) ([]*models.Artist, error) {
|
||||
|
|
@ -36,7 +38,7 @@ func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) (
|
|||
l.Debug().Msg("Associating artists by MusicBrainz ID(s) mappings")
|
||||
mbzMatches, err := matchArtistsByMBIDMappings(ctx, d, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("AssociateArtists: %w", err)
|
||||
}
|
||||
result = append(result, mbzMatches...)
|
||||
}
|
||||
|
|
@ -45,16 +47,16 @@ func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) (
|
|||
l.Debug().Msg("Associating artists by list of MusicBrainz ID(s)")
|
||||
mbzMatches, err := matchArtistsByMBID(ctx, d, opts, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("AssociateArtists: %w", err)
|
||||
}
|
||||
result = append(result, mbzMatches...)
|
||||
}
|
||||
|
||||
if len(opts.ArtistNames) > len(result) {
|
||||
l.Debug().Msg("Associating artists by list of artist names")
|
||||
nameMatches, err := matchArtistsByNames(ctx, opts.ArtistNames, result, d)
|
||||
nameMatches, err := matchArtistsByNames(ctx, opts.ArtistNames, result, d, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("AssociateArtists: %w", err)
|
||||
}
|
||||
result = append(result, nameMatches...)
|
||||
}
|
||||
|
|
@ -62,9 +64,9 @@ func AssociateArtists(ctx context.Context, d db.DB, opts AssociateArtistsOpts) (
|
|||
if len(result) < 1 {
|
||||
allArtists := slices.Concat(opts.ArtistNames, ParseArtists(opts.ArtistName, opts.TrackTitle))
|
||||
l.Debug().Msgf("Associating artists by artist name(s) %v and track title '%s'", allArtists, opts.TrackTitle)
|
||||
fallbackMatches, err := matchArtistsByNames(ctx, allArtists, nil, d)
|
||||
fallbackMatches, err := matchArtistsByNames(ctx, allArtists, nil, d, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("AssociateArtists: %w", err)
|
||||
}
|
||||
result = append(result, fallbackMatches...)
|
||||
}
|
||||
|
|
@ -77,7 +79,6 @@ func matchArtistsByMBIDMappings(ctx context.Context, d db.DB, opts AssociateArti
|
|||
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,
|
||||
})
|
||||
|
|
@ -87,18 +88,17 @@ func matchArtistsByMBIDMappings(ctx context.Context, d db.DB, opts AssociateArti
|
|||
continue
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("matchArtistsBYMBIDMappings: %w", err)
|
||||
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)
|
||||
l.Err(err).Msgf("matchArtistsByMBIDMappings: Failed to associate artist '%s' with MusicBrainz ID", artist.Name)
|
||||
} else {
|
||||
artist.MbzID = &a.Mbid
|
||||
}
|
||||
|
|
@ -106,36 +106,51 @@ func matchArtistsByMBIDMappings(ctx context.Context, d db.DB, opts AssociateArti
|
|||
continue
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("matchArtistsBYMBIDMappings: %w", err)
|
||||
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)
|
||||
artist, err = resolveAliasOrCreateArtist(ctx, a.Mbid, opts.ArtistNames, d, opts)
|
||||
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")
|
||||
l.Warn().AnErr("error", err).Msg("matchArtistsByMBIDMappings: MusicBrainz unreachable, creating new artist with provided MusicBrainz ID mapping")
|
||||
|
||||
var imgid uuid.UUID
|
||||
imgUrl, err := images.GetArtistImage(ctx, images.ArtistImageOpts{
|
||||
imgUrl, imgErr := images.GetArtistImage(ctx, images.ArtistImageOpts{
|
||||
Aliases: []string{a.Artist},
|
||||
})
|
||||
if err == nil {
|
||||
if imgErr == nil && imgUrl != "" {
|
||||
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
|
||||
if !opts.SkipCacheImage {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
l.Debug().Msg("Downloading artist image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
l.Err(fmt.Errorf("matchArtistsByMBIDMappings: %w", err)).Msgf("Failed to get artist image for artist '%s'", a.Artist)
|
||||
l.Err(imgErr).Msgf("matchArtistsByMBIDMappings: 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})
|
||||
|
||||
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)
|
||||
l.Err(err).Msgf("matchArtistsByMBIDMappings: Failed to create artist '%s' in database", a.Artist)
|
||||
return nil, fmt.Errorf("matchArtistsByMBIDMappings: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, artist)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +165,7 @@ func matchArtistsByMBID(ctx context.Context, d db.DB, opts AssociateArtistsOpts,
|
|||
}
|
||||
if id == uuid.Nil {
|
||||
l.Warn().Msg("Provided artist has uuid.Nil MusicBrainzID")
|
||||
return matchArtistsByNames(ctx, opts.ArtistNames, result, d)
|
||||
return matchArtistsByNames(ctx, opts.ArtistNames, result, d, opts)
|
||||
}
|
||||
a, err := d.GetArtist(ctx, db.GetArtistOpts{
|
||||
MusicBrainzID: id,
|
||||
|
|
@ -160,7 +175,6 @@ func matchArtistsByMBID(ctx context.Context, d db.DB, opts AssociateArtistsOpts,
|
|||
result = append(result, a)
|
||||
continue
|
||||
}
|
||||
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -168,22 +182,25 @@ func matchArtistsByMBID(ctx context.Context, d db.DB, opts AssociateArtistsOpts,
|
|||
if len(opts.ArtistNames) < 1 {
|
||||
opts.ArtistNames = slices.Concat(opts.ArtistNames, ParseArtists(opts.ArtistName, opts.TrackTitle))
|
||||
}
|
||||
a, err = resolveAliasOrCreateArtist(ctx, id, opts.ArtistNames, d, opts.Mbzc)
|
||||
|
||||
a, err = resolveAliasOrCreateArtist(ctx, id, opts.ArtistNames, d, opts)
|
||||
if err != nil {
|
||||
l.Warn().Msg("MusicBrainz unreachable, falling back to artist name matching")
|
||||
return matchArtistsByNames(ctx, opts.ArtistNames, result, d)
|
||||
// return nil, err
|
||||
return matchArtistsByNames(ctx, opts.ArtistNames, result, d, opts)
|
||||
}
|
||||
|
||||
result = append(result, a)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
func resolveAliasOrCreateArtist(ctx context.Context, mbzID uuid.UUID, names []string, d db.DB, mbz mbz.MusicBrainzCaller) (*models.Artist, error) {
|
||||
|
||||
func resolveAliasOrCreateArtist(ctx context.Context, mbzID uuid.UUID, names []string, d db.DB, opts AssociateArtistsOpts) (*models.Artist, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
|
||||
aliases, err := mbz.GetArtistPrimaryAliases(ctx, mbzID)
|
||||
aliases, err := opts.Mbzc.GetArtistPrimaryAliases(ctx, mbzID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("resolveAliasOrCreateArtist: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Got aliases %v from MusicBrainz", aliases)
|
||||
|
||||
|
|
@ -195,10 +212,10 @@ func resolveAliasOrCreateArtist(ctx context.Context, mbzID uuid.UUID, names []st
|
|||
a.MbzID = &mbzID
|
||||
l.Debug().Msgf("Alias '%s' found in DB. Associating with MusicBrainz ID...", alias)
|
||||
if updateErr := d.UpdateArtist(ctx, db.UpdateArtistOpts{ID: a.ID, MusicBrainzID: mbzID}); updateErr != nil {
|
||||
return nil, updateErr
|
||||
return nil, fmt.Errorf("resolveAliasOrCreateArtist: %w", updateErr)
|
||||
}
|
||||
if saveAliasErr := d.SaveArtistAliases(ctx, a.ID, aliases, "MusicBrainz"); saveAliasErr != nil {
|
||||
return nil, saveAliasErr
|
||||
return nil, fmt.Errorf("resolveAliasOrCreateArtist: %w", saveAliasErr)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
|
@ -220,20 +237,22 @@ func resolveAliasOrCreateArtist(ctx context.Context, mbzID uuid.UUID, names []st
|
|||
Aliases: aliases,
|
||||
})
|
||||
if err == nil && imgUrl != "" {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
imgid = uuid.New()
|
||||
l.Debug().Msg("Downloading artist image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
if !opts.SkipCacheImage {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
l.Debug().Msg("Downloading artist image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
l.Warn().Msgf("Failed to get artist image from ImageSrc: %s", err.Error())
|
||||
l.Warn().AnErr("error", err).Msg("Failed to get artist image from ImageSrc")
|
||||
}
|
||||
|
||||
u, err := d.SaveArtist(ctx, db.SaveArtistOpts{
|
||||
|
|
@ -244,13 +263,13 @@ func resolveAliasOrCreateArtist(ctx context.Context, mbzID uuid.UUID, names []st
|
|||
ImageSrc: imgUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("resolveAliasOrCreateArtist: %w", err)
|
||||
}
|
||||
l.Info().Msgf("Created artist '%s' with MusicBrainz Artist ID", canonical)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func matchArtistsByNames(ctx context.Context, names []string, existing []*models.Artist, d db.DB) ([]*models.Artist, error) {
|
||||
func matchArtistsByNames(ctx context.Context, names []string, existing []*models.Artist, d db.DB, opts AssociateArtistsOpts) ([]*models.Artist, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
var result []*models.Artist
|
||||
|
||||
|
|
@ -273,29 +292,31 @@ func matchArtistsByNames(ctx context.Context, names []string, existing []*models
|
|||
Aliases: []string{name},
|
||||
})
|
||||
if err == nil && imgUrl != "" {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
imgid = uuid.New()
|
||||
l.Debug().Msg("Downloading artist image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
if !opts.SkipCacheImage {
|
||||
var size ImageSize
|
||||
if cfg.FullImageCacheEnabled() {
|
||||
size = ImageSizeFull
|
||||
} else {
|
||||
size = ImageSizeLarge
|
||||
}
|
||||
l.Debug().Msg("Downloading artist image from source...")
|
||||
err = DownloadAndCacheImage(ctx, imgid, imgUrl, size)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to cache image")
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
l.Debug().Msgf("Failed to get artist images for %s: %s", name, err.Error())
|
||||
l.Debug().AnErr("error", err).Msgf("Failed to get artist images for %s", name)
|
||||
}
|
||||
a, err = d.SaveArtist(ctx, db.SaveArtistOpts{Name: name, Image: imgid, ImageSrc: imgUrl})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchArtistsByNames: %w", err)
|
||||
}
|
||||
l.Info().Msgf("Created artist '%s' with artist name", name)
|
||||
result = append(result, a)
|
||||
} else {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchArtistsByNames: %w", err)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package catalog
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
|
|
@ -24,13 +25,13 @@ type AssociateTrackOpts struct {
|
|||
func AssociateTrack(ctx context.Context, d db.DB, opts AssociateTrackOpts) (*models.Track, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
if opts.TrackName == "" {
|
||||
return nil, errors.New("missing required parameter 'opts.TrackName'")
|
||||
return nil, errors.New("AssociateTrack: missing required parameter 'opts.TrackName'")
|
||||
}
|
||||
if len(opts.ArtistIDs) < 1 {
|
||||
return nil, errors.New("at least one artist id must be specified")
|
||||
return nil, errors.New("AssociateTrack: at least one artist id must be specified")
|
||||
}
|
||||
if opts.AlbumID == 0 {
|
||||
return nil, errors.New("release group id must be specified")
|
||||
return nil, errors.New("AssociateTrack: release group id must be specified")
|
||||
}
|
||||
// first, try to match track Mbz ID
|
||||
if opts.TrackMbzID != uuid.Nil {
|
||||
|
|
@ -52,12 +53,12 @@ func matchTrackByMbzID(ctx context.Context, d db.DB, opts AssociateTrackOpts) (*
|
|||
l.Debug().Msgf("Found track '%s' by MusicBrainz ID", track.Title)
|
||||
return track, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchTrackByMbzID: %w", err)
|
||||
} else {
|
||||
l.Debug().Msgf("Track '%s' could not be found by MusicBrainz ID", opts.TrackName)
|
||||
track, err := matchTrackByTitleAndArtist(ctx, d, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchTrackByMbzID: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Updating track '%s' with MusicBrainz ID %s", opts.TrackName, opts.TrackMbzID)
|
||||
err = d.UpdateTrack(ctx, db.UpdateTrackOpts{
|
||||
|
|
@ -65,7 +66,7 @@ func matchTrackByMbzID(ctx context.Context, d db.DB, opts AssociateTrackOpts) (*
|
|||
MusicBrainzID: opts.TrackMbzID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchTrackByMbzID: %w", err)
|
||||
}
|
||||
track.MbzID = &opts.TrackMbzID
|
||||
return track, nil
|
||||
|
|
@ -83,7 +84,7 @@ func matchTrackByTitleAndArtist(ctx context.Context, d db.DB, opts AssociateTrac
|
|||
l.Debug().Msgf("Track '%s' found by title and artist match", track.Title)
|
||||
return track, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchTrackByTitleAndArtist: %w", err)
|
||||
} else {
|
||||
if opts.TrackMbzID != uuid.Nil {
|
||||
mbzTrack, err := opts.Mbzc.GetTrack(ctx, opts.TrackMbzID)
|
||||
|
|
@ -107,7 +108,7 @@ func matchTrackByTitleAndArtist(ctx context.Context, d db.DB, opts AssociateTrac
|
|||
Duration: opts.Duration,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("matchTrackByTitleAndArtist: %w", err)
|
||||
}
|
||||
if opts.TrackMbzID == uuid.Nil {
|
||||
l.Info().Msgf("Created track '%s' with title and artist", opts.TrackName)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package catalog
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -39,6 +40,9 @@ type SubmitListenOpts struct {
|
|||
// artist, release, release group, and track in DB
|
||||
SkipSaveListen bool
|
||||
|
||||
// When true, skips caching the images and only stores the image url in the db
|
||||
SkipCacheImage bool
|
||||
|
||||
MbzCaller mbz.MusicBrainzCaller
|
||||
ArtistNames []string
|
||||
Artist string
|
||||
|
|
@ -51,8 +55,9 @@ type SubmitListenOpts struct {
|
|||
ReleaseMbzID uuid.UUID
|
||||
ReleaseGroupMbzID uuid.UUID
|
||||
Time time.Time
|
||||
UserID int32
|
||||
Client string
|
||||
|
||||
UserID int32
|
||||
Client string
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -70,16 +75,17 @@ func SubmitListen(ctx context.Context, store db.DB, opts SubmitListenOpts) error
|
|||
ctx,
|
||||
store,
|
||||
AssociateArtistsOpts{
|
||||
ArtistMbzIDs: opts.ArtistMbzIDs,
|
||||
ArtistNames: opts.ArtistNames,
|
||||
ArtistName: opts.Artist,
|
||||
ArtistMbidMap: opts.ArtistMbidMappings,
|
||||
Mbzc: opts.MbzCaller,
|
||||
TrackTitle: opts.TrackTitle,
|
||||
ArtistMbzIDs: opts.ArtistMbzIDs,
|
||||
ArtistNames: opts.ArtistNames,
|
||||
ArtistName: opts.Artist,
|
||||
ArtistMbidMap: opts.ArtistMbidMappings,
|
||||
Mbzc: opts.MbzCaller,
|
||||
TrackTitle: opts.TrackTitle,
|
||||
SkipCacheImage: opts.SkipCacheImage,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Failed to associate artists to listen")
|
||||
return err
|
||||
l.Err(err).Msg("Failed to associate artists to listen")
|
||||
return fmt.Errorf("SubmitListen: %w", err)
|
||||
} else if len(artists) < 1 {
|
||||
l.Debug().Msg("Failed to associate any artists to release")
|
||||
}
|
||||
|
|
@ -97,10 +103,11 @@ func SubmitListen(ctx context.Context, store db.DB, opts SubmitListenOpts) error
|
|||
TrackName: opts.TrackTitle,
|
||||
Mbzc: opts.MbzCaller,
|
||||
Artists: artists,
|
||||
SkipCacheImage: opts.SkipCacheImage,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Failed to associate release group to listen")
|
||||
return err
|
||||
return fmt.Errorf("SubmitListen: %w", err)
|
||||
}
|
||||
l.Debug().Any("album", rg).Msg("Matched listen to release")
|
||||
|
||||
|
|
@ -120,7 +127,7 @@ func SubmitListen(ctx context.Context, store db.DB, opts SubmitListenOpts) error
|
|||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Failed to associate track to listen")
|
||||
return err
|
||||
return fmt.Errorf("SubmitListen: %w", err)
|
||||
}
|
||||
l.Debug().Any("track", track).Msg("Matched listen to track")
|
||||
|
||||
|
|
|
|||
|
|
@ -82,17 +82,17 @@ func SourceImageDir() string {
|
|||
func ValidateImageURL(url string) error {
|
||||
resp, err := http.Head(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to perform HEAD request: %w", err)
|
||||
return fmt.Errorf("ValidateImageURL: http.Head: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HEAD request failed, status code: %d", resp.StatusCode)
|
||||
return fmt.Errorf("ValidateImageURL: HEAD request failed, status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return fmt.Errorf("URL does not point to an image, content type: %s", contentType)
|
||||
return fmt.Errorf("ValidateImageURL: URL does not point to an image, content type: %s", contentType)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -103,20 +103,24 @@ func DownloadAndCacheImage(ctx context.Context, id uuid.UUID, url string, size I
|
|||
l := logger.FromContext(ctx)
|
||||
err := ValidateImageURL(url)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("DownloadAndCacheImage: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Downloading image for ID %s", id)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download image: %w", err)
|
||||
return fmt.Errorf("DownloadAndCacheImage: http.Get: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download image, status code: %d", resp.StatusCode)
|
||||
return fmt.Errorf("DownloadAndCacheImage: failed to download image, status: %s", resp.Status)
|
||||
}
|
||||
|
||||
return CompressAndSaveImage(ctx, id.String(), size, resp.Body)
|
||||
err = CompressAndSaveImage(ctx, id.String(), size, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DownloadAndCacheImage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compresses an image to the specified size, then saves it to the correct cache folder.
|
||||
|
|
@ -124,16 +128,24 @@ func CompressAndSaveImage(ctx context.Context, filename string, size ImageSize,
|
|||
l := logger.FromContext(ctx)
|
||||
|
||||
if size == ImageSizeFull {
|
||||
return saveImage(filename, size, body)
|
||||
err := saveImage(filename, size, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CompressAndSaveImage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Debug().Msg("Creating resized image")
|
||||
compressed, err := compressImage(size, body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("CompressAndSaveImage: %w", err)
|
||||
}
|
||||
|
||||
return saveImage(filename, size, compressed)
|
||||
err = saveImage(filename, size, compressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CompressAndSaveImage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImage saves an image to the image_cache/{size} folder
|
||||
|
|
@ -144,21 +156,21 @@ func saveImage(filename string, size ImageSize, data io.Reader) error {
|
|||
// Ensure the cache directory exists
|
||||
err := os.MkdirAll(filepath.Join(cacheDir, string(size)), 0744)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create full image cache directory: %w", err)
|
||||
return fmt.Errorf("saveImage: failed to create full image cache directory: %w", err)
|
||||
}
|
||||
|
||||
// Create a file in the cache directory
|
||||
imagePath := filepath.Join(cacheDir, string(size), filename)
|
||||
file, err := os.Create(imagePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create image file: %w", err)
|
||||
return fmt.Errorf("saveImage: failed to create image file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Save the image to the file
|
||||
_, err = io.Copy(file, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save image: %w", err)
|
||||
return fmt.Errorf("saveImage: failed to save image: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -167,7 +179,7 @@ func saveImage(filename string, size ImageSize, data io.Reader) error {
|
|||
func compressImage(size ImageSize, data io.Reader) (io.Reader, error) {
|
||||
imgBytes, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("compressImage: io.ReadAll: %w", err)
|
||||
}
|
||||
px := GetImageSize(size)
|
||||
// Resize with bimg
|
||||
|
|
@ -180,10 +192,10 @@ func compressImage(size ImageSize, data io.Reader) (io.Reader, error) {
|
|||
Type: bimg.WEBP,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("compressImage: bimg.NewImage: %w", err)
|
||||
}
|
||||
if len(imgBytes) == 0 {
|
||||
return nil, fmt.Errorf("compression failed")
|
||||
return nil, fmt.Errorf("compressImage: failed to compress image: %w", err)
|
||||
}
|
||||
return bytes.NewReader(imgBytes), nil
|
||||
}
|
||||
|
|
@ -198,19 +210,19 @@ func DeleteImage(filename uuid.UUID) error {
|
|||
// }
|
||||
err := os.Remove(path.Join(cacheDir, "full", filename.String()))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
return fmt.Errorf("DeleteImage: %w", err)
|
||||
}
|
||||
err = os.Remove(path.Join(cacheDir, "large", filename.String()))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
return fmt.Errorf("DeleteImage: %w", err)
|
||||
}
|
||||
err = os.Remove(path.Join(cacheDir, "medium", filename.String()))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
return fmt.Errorf("DeleteImage: %w", err)
|
||||
}
|
||||
err = os.Remove(path.Join(cacheDir, "small", filename.String()))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
return fmt.Errorf("DeleteImage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -230,7 +242,7 @@ func PruneOrphanedImages(ctx context.Context, store db.DB) error {
|
|||
for _, dir := range []string{"large", "medium", "small", "full"} {
|
||||
c, err := pruneDirImgs(ctx, store, path.Join(cacheDir, dir), memo)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("PruneOrphanedImages: %w", err)
|
||||
}
|
||||
count += c
|
||||
}
|
||||
|
|
@ -256,7 +268,7 @@ func pruneDirImgs(ctx context.Context, store db.DB, path string, memo map[string
|
|||
}
|
||||
exists, err := store.ImageHasAssociation(ctx, imageid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("pruneDirImages: %w", err)
|
||||
} else if exists {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue