mirror of https://github.com/gabehf/Koito.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
8.7 KiB
266 lines
8.7 KiB
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/gabehf/koito/internal/cfg"
|
|
"github.com/gabehf/koito/internal/db"
|
|
"github.com/gabehf/koito/internal/images"
|
|
"github.com/gabehf/koito/internal/logger"
|
|
"github.com/gabehf/koito/internal/mbz"
|
|
"github.com/gabehf/koito/internal/models"
|
|
"github.com/gabehf/koito/internal/utils"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type AssociateAlbumOpts struct {
|
|
Artists []*models.Artist
|
|
ReleaseMbzID uuid.UUID
|
|
ReleaseGroupMbzID uuid.UUID
|
|
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("AssociateAlbum: required parameter TrackName missing")
|
|
}
|
|
releaseTitle := opts.ReleaseName
|
|
if releaseTitle == "" {
|
|
releaseTitle = opts.TrackName
|
|
}
|
|
if opts.ReleaseMbzID != uuid.Nil {
|
|
l.Debug().Msgf("Associating album '%s' by MusicBrainz release ID", releaseTitle)
|
|
return matchAlbumByMbzReleaseID(ctx, d, opts)
|
|
} else {
|
|
l.Debug().Msgf("Associating album '%s' by title and artist", releaseTitle)
|
|
return matchAlbumByTitle(ctx, d, opts)
|
|
}
|
|
}
|
|
|
|
func matchAlbumByMbzReleaseID(ctx context.Context, d db.DB, opts AssociateAlbumOpts) (*models.Album, error) {
|
|
l := logger.FromContext(ctx)
|
|
a, err := d.GetAlbum(ctx, db.GetAlbumOpts{MusicBrainzID: opts.ReleaseMbzID})
|
|
if err == nil {
|
|
l.Debug().Msgf("Found release '%s' by MusicBrainz Release ID", a.Title)
|
|
return &models.Album{
|
|
ID: a.ID,
|
|
MbzID: &opts.ReleaseMbzID,
|
|
Title: a.Title,
|
|
VariousArtists: a.VariousArtists,
|
|
Image: a.Image,
|
|
}, nil
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
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)
|
|
if err != nil {
|
|
return matchAlbumByTitle(ctx, d, opts)
|
|
}
|
|
return rg, nil
|
|
}
|
|
}
|
|
|
|
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("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,
|
|
Titles: titles,
|
|
})
|
|
if err == nil {
|
|
l.Debug().Msgf("Found album %s, updating with MusicBrainz Release ID...", album.Title)
|
|
err := d.UpdateAlbum(ctx, db.UpdateAlbumOpts{
|
|
ID: album.ID,
|
|
MusicBrainzID: opts.ReleaseMbzID,
|
|
})
|
|
if err != nil {
|
|
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("createOrUpdateAlbumWithMbzReleaseID: failed to save aliases")
|
|
}
|
|
} else {
|
|
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("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" {
|
|
l.Debug().Msgf("MusicBrainz release group '%s' detected as being a Various Artists compilation release", release.Title)
|
|
variousArtists = true
|
|
}
|
|
}
|
|
|
|
l.Debug().Msg("Searching for album images...")
|
|
var imgid uuid.UUID
|
|
imgUrl, err := images.GetAlbumImage(ctx, images.AlbumImageOpts{
|
|
Artists: utils.UniqueIgnoringCase(slices.Concat(utils.FlattenMbzArtistCreditNames(release.ArtistCredit), utils.FlattenArtistNames(opts.Artists))),
|
|
Album: release.Title,
|
|
ReleaseMbzID: &opts.ReleaseMbzID,
|
|
})
|
|
|
|
if err == nil && imgUrl != "" {
|
|
imgid = uuid.New()
|
|
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("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,
|
|
ArtistIDs: utils.FlattenArtistIDs(opts.Artists),
|
|
VariousArtists: variousArtists,
|
|
Image: imgid,
|
|
ImageSrc: imgUrl,
|
|
})
|
|
if err != nil {
|
|
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("createOrUpdateAlbumWithMbzReleaseID: failed to save aliases")
|
|
}
|
|
} else {
|
|
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,
|
|
Title: album.Title,
|
|
VariousArtists: album.VariousArtists,
|
|
}, nil
|
|
}
|
|
|
|
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,
|
|
})
|
|
if err == nil {
|
|
l.Debug().Msgf("Found album '%s' by artist and title", a.Title)
|
|
if a.MbzID == nil && opts.ReleaseMbzID != uuid.Nil {
|
|
l.Debug().Msgf("Updating album with id %d with MusicBrainz ID %s", a.ID, opts.ReleaseMbzID)
|
|
err = d.UpdateAlbum(ctx, db.UpdateAlbumOpts{
|
|
ID: a.ID,
|
|
MusicBrainzID: opts.ReleaseMbzID,
|
|
})
|
|
if err != nil {
|
|
l.Err(err).Msg("matchAlbumByTitle: failed to associate existing release with MusicBrainz ID")
|
|
}
|
|
}
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, fmt.Errorf("matchAlbumByTitle: %w", err)
|
|
} else {
|
|
var imgid uuid.UUID
|
|
imgUrl, err := images.GetAlbumImage(ctx, images.AlbumImageOpts{
|
|
Artists: utils.FlattenArtistNames(opts.Artists),
|
|
Album: opts.ReleaseName,
|
|
ReleaseMbzID: &opts.ReleaseMbzID,
|
|
})
|
|
if err == nil && imgUrl != "" {
|
|
imgid = uuid.New()
|
|
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().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),
|
|
Image: imgid,
|
|
MusicBrainzID: opts.ReleaseMbzID,
|
|
ImageSrc: imgUrl,
|
|
})
|
|
if err != nil {
|
|
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,
|
|
}, nil
|
|
}
|