mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 13:38:15 -08: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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,30 +17,31 @@ const (
|
|||
|
||||
const (
|
||||
// BASE_URL_ENV = "KOITO_BASE_URL"
|
||||
DATABASE_URL_ENV = "KOITO_DATABASE_URL"
|
||||
BIND_ADDR_ENV = "KOITO_BIND_ADDR"
|
||||
LISTEN_PORT_ENV = "KOITO_LISTEN_PORT"
|
||||
ENABLE_STRUCTURED_LOGGING_ENV = "KOITO_ENABLE_STRUCTURED_LOGGING"
|
||||
ENABLE_FULL_IMAGE_CACHE_ENV = "KOITO_ENABLE_FULL_IMAGE_CACHE"
|
||||
LOG_LEVEL_ENV = "KOITO_LOG_LEVEL"
|
||||
MUSICBRAINZ_URL_ENV = "KOITO_MUSICBRAINZ_URL"
|
||||
MUSICBRAINZ_RATE_LIMIT_ENV = "KOITO_MUSICBRAINZ_RATE_LIMIT"
|
||||
ENABLE_LBZ_RELAY_ENV = "KOITO_ENABLE_LBZ_RELAY"
|
||||
LBZ_RELAY_URL_ENV = "KOITO_LBZ_RELAY_URL"
|
||||
LBZ_RELAY_TOKEN_ENV = "KOITO_LBZ_RELAY_TOKEN"
|
||||
CONFIG_DIR_ENV = "KOITO_CONFIG_DIR"
|
||||
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
||||
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
||||
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
||||
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
||||
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
||||
SKIP_IMPORT_ENV = "KOITO_SKIP_IMPORT"
|
||||
ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS"
|
||||
CORS_ORIGINS_ENV = "KOITO_CORS_ALLOWED_ORIGINS"
|
||||
DISABLE_RATE_LIMIT_ENV = "KOITO_DISABLE_RATE_LIMIT"
|
||||
THROTTLE_IMPORTS_MS = "KOITO_THROTTLE_IMPORTS_MS"
|
||||
IMPORT_BEFORE_UNIX_ENV = "KOITO_IMPORT_BEFORE_UNIX"
|
||||
IMPORT_AFTER_UNIX_ENV = "KOITO_IMPORT_AFTER_UNIX"
|
||||
DATABASE_URL_ENV = "KOITO_DATABASE_URL"
|
||||
BIND_ADDR_ENV = "KOITO_BIND_ADDR"
|
||||
LISTEN_PORT_ENV = "KOITO_LISTEN_PORT"
|
||||
ENABLE_STRUCTURED_LOGGING_ENV = "KOITO_ENABLE_STRUCTURED_LOGGING"
|
||||
ENABLE_FULL_IMAGE_CACHE_ENV = "KOITO_ENABLE_FULL_IMAGE_CACHE"
|
||||
LOG_LEVEL_ENV = "KOITO_LOG_LEVEL"
|
||||
MUSICBRAINZ_URL_ENV = "KOITO_MUSICBRAINZ_URL"
|
||||
MUSICBRAINZ_RATE_LIMIT_ENV = "KOITO_MUSICBRAINZ_RATE_LIMIT"
|
||||
ENABLE_LBZ_RELAY_ENV = "KOITO_ENABLE_LBZ_RELAY"
|
||||
LBZ_RELAY_URL_ENV = "KOITO_LBZ_RELAY_URL"
|
||||
LBZ_RELAY_TOKEN_ENV = "KOITO_LBZ_RELAY_TOKEN"
|
||||
CONFIG_DIR_ENV = "KOITO_CONFIG_DIR"
|
||||
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
||||
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
||||
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
||||
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
||||
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
||||
SKIP_IMPORT_ENV = "KOITO_SKIP_IMPORT"
|
||||
ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS"
|
||||
CORS_ORIGINS_ENV = "KOITO_CORS_ALLOWED_ORIGINS"
|
||||
DISABLE_RATE_LIMIT_ENV = "KOITO_DISABLE_RATE_LIMIT"
|
||||
THROTTLE_IMPORTS_MS = "KOITO_THROTTLE_IMPORTS_MS"
|
||||
IMPORT_BEFORE_UNIX_ENV = "KOITO_IMPORT_BEFORE_UNIX"
|
||||
IMPORT_AFTER_UNIX_ENV = "KOITO_IMPORT_AFTER_UNIX"
|
||||
FETCH_IMAGES_DURING_IMPORT_ENV = "KOITO_FETCH_IMAGES_DURING_IMPORT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
|
@ -48,29 +49,30 @@ type config struct {
|
|||
listenPort int
|
||||
configDir string
|
||||
// baseUrl string
|
||||
databaseUrl string
|
||||
musicBrainzUrl string
|
||||
musicBrainzRateLimit int
|
||||
logLevel int
|
||||
structuredLogging bool
|
||||
enableFullImageCache bool
|
||||
lbzRelayEnabled bool
|
||||
lbzRelayUrl string
|
||||
lbzRelayToken string
|
||||
defaultPw string
|
||||
defaultUsername string
|
||||
disableDeezer bool
|
||||
disableCAA bool
|
||||
disableMusicBrainz bool
|
||||
skipImport bool
|
||||
allowedHosts []string
|
||||
allowAllHosts bool
|
||||
allowedOrigins []string
|
||||
disableRateLimit bool
|
||||
importThrottleMs int
|
||||
userAgent string
|
||||
importBefore time.Time
|
||||
importAfter time.Time
|
||||
databaseUrl string
|
||||
musicBrainzUrl string
|
||||
musicBrainzRateLimit int
|
||||
logLevel int
|
||||
structuredLogging bool
|
||||
enableFullImageCache bool
|
||||
lbzRelayEnabled bool
|
||||
lbzRelayUrl string
|
||||
lbzRelayToken string
|
||||
defaultPw string
|
||||
defaultUsername string
|
||||
disableDeezer bool
|
||||
disableCAA bool
|
||||
disableMusicBrainz bool
|
||||
skipImport bool
|
||||
fetchImageDuringImport bool
|
||||
allowedHosts []string
|
||||
allowAllHosts bool
|
||||
allowedOrigins []string
|
||||
disableRateLimit bool
|
||||
importThrottleMs int
|
||||
userAgent string
|
||||
importBefore time.Time
|
||||
importAfter time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -85,7 +87,10 @@ func Load(getenv func(string) string, version string) error {
|
|||
once.Do(func() {
|
||||
globalConfig, err = loadConfig(getenv, version)
|
||||
})
|
||||
return err
|
||||
if err != nil {
|
||||
return fmt.Errorf("cfg.Load: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig loads the configuration from environment variables.
|
||||
|
|
@ -94,7 +99,7 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
|
|||
|
||||
cfg.databaseUrl = getenv(DATABASE_URL_ENV)
|
||||
if cfg.databaseUrl == "" {
|
||||
return nil, errors.New("required parameter " + DATABASE_URL_ENV + " not provided")
|
||||
return nil, errors.New("loadConfig: required parameter " + DATABASE_URL_ENV + " not provided")
|
||||
}
|
||||
cfg.bindAddr = getenv(BIND_ADDR_ENV)
|
||||
var err error
|
||||
|
|
@ -136,6 +141,7 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
|
|||
cfg.disableRateLimit = parseBool(getenv(DISABLE_RATE_LIMIT_ENV))
|
||||
|
||||
cfg.structuredLogging = parseBool(getenv(ENABLE_STRUCTURED_LOGGING_ENV))
|
||||
cfg.fetchImageDuringImport = parseBool(getenv(FETCH_IMAGES_DURING_IMPORT_ENV))
|
||||
|
||||
cfg.enableFullImageCache = parseBool(getenv(ENABLE_FULL_IMAGE_CACHE_ENV))
|
||||
cfg.disableDeezer = parseBool(getenv(DISABLE_DEEZER_ENV))
|
||||
|
|
@ -211,12 +217,6 @@ func ConfigDir() string {
|
|||
return globalConfig.configDir
|
||||
}
|
||||
|
||||
// func BaseUrl() string {
|
||||
// lock.RLock()
|
||||
// defer lock.RUnlock()
|
||||
// return globalConfig.baseUrl
|
||||
// }
|
||||
|
||||
func DatabaseUrl() string {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
|
@ -339,5 +339,13 @@ func ThrottleImportMs() int {
|
|||
|
||||
// returns the before, after times, in that order
|
||||
func ImportWindow() (time.Time, time.Time) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
return globalConfig.importBefore, globalConfig.importAfter
|
||||
}
|
||||
|
||||
func FetchImagesDuringImport() bool {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
return globalConfig.fetchImageDuringImport
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ type DB interface {
|
|||
GetArtist(ctx context.Context, opts GetArtistOpts) (*models.Artist, error)
|
||||
GetAlbum(ctx context.Context, opts GetAlbumOpts) (*models.Album, error)
|
||||
GetTrack(ctx context.Context, opts GetTrackOpts) (*models.Track, error)
|
||||
GetArtistsForAlbum(ctx context.Context, id int32) ([]*models.Artist, error)
|
||||
GetArtistsForTrack(ctx context.Context, id int32) ([]*models.Artist, error)
|
||||
GetTopTracksPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Track], error)
|
||||
GetTopArtistsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Artist], error)
|
||||
GetTopAlbumsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Album], error)
|
||||
|
|
@ -48,6 +50,8 @@ type DB interface {
|
|||
SetPrimaryArtistAlias(ctx context.Context, id int32, alias string) error
|
||||
SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string) error
|
||||
SetPrimaryTrackAlias(ctx context.Context, id int32, alias string) error
|
||||
SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error
|
||||
SetPrimaryTrackArtist(ctx context.Context, id int32, artistId int32, value bool) error
|
||||
// Delete
|
||||
DeleteArtist(ctx context.Context, id int32) error
|
||||
DeleteAlbum(ctx context.Context, id int32) error
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -41,11 +42,11 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
|
|||
Column1: opts.Titles,
|
||||
})
|
||||
} else {
|
||||
return nil, errors.New("insufficient information to get album")
|
||||
return nil, errors.New("GetAlbum: insufficient information to get album")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAlbum: %w", err)
|
||||
}
|
||||
|
||||
count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
|
||||
|
|
@ -54,7 +55,7 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
|
|||
ReleaseID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err)
|
||||
}
|
||||
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
|
|
@ -62,7 +63,7 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
|
|||
AlbumID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
|
||||
}
|
||||
|
||||
return &models.Album{
|
||||
|
|
@ -87,17 +88,17 @@ func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Al
|
|||
insertImage = &opts.Image
|
||||
}
|
||||
if len(opts.ArtistIDs) < 1 {
|
||||
return nil, errors.New("required parameter 'ArtistIDs' missing")
|
||||
return nil, errors.New("SaveAlbum: required parameter 'ArtistIDs' missing")
|
||||
}
|
||||
for _, aid := range opts.ArtistIDs {
|
||||
if aid == 0 {
|
||||
return nil, errors.New("none of 'ArtistIDs' may be 0")
|
||||
return nil, errors.New("SaveAlbum: none of 'ArtistIDs' may be 0")
|
||||
}
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveAlbum: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -109,7 +110,7 @@ func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Al
|
|||
ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveAlbum: InsertRelease: %w", err)
|
||||
}
|
||||
for _, artistId := range opts.ArtistIDs {
|
||||
l.Debug().Msgf("Associating release '%s' to artist with ID %d", opts.Title, artistId)
|
||||
|
|
@ -118,7 +119,7 @@ func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Al
|
|||
ReleaseID: r.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveAlbum: AssociateArtistToRelease: %w", err)
|
||||
}
|
||||
}
|
||||
l.Debug().Msgf("Saving canonical alias %s for release %d", opts.Title, r.ID)
|
||||
|
|
@ -130,11 +131,12 @@ func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Al
|
|||
})
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Failed to save canonical alias for album %d", r.ID)
|
||||
return nil, fmt.Errorf("SaveAlbum: InsertReleaseAlias: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveAlbum: Commit: %w", err)
|
||||
}
|
||||
|
||||
return &models.Album{
|
||||
|
|
@ -151,7 +153,7 @@ func (d *Psql) AddArtistsToAlbum(ctx context.Context, opts db.AddArtistsToAlbumO
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("AddArtistsToAlbum: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -162,6 +164,7 @@ func (d *Psql) AddArtistsToAlbum(ctx context.Context, opts db.AddArtistsToAlbumO
|
|||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msgf("Failed to associate release %d with artist %d", opts.AlbumID, id)
|
||||
return fmt.Errorf("AddArtistsToAlbum: AssociateArtistToRelease: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -175,7 +178,7 @@ func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error {
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("UpdateAlbum: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -186,7 +189,7 @@ func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error {
|
|||
MusicBrainzID: &opts.MusicBrainzID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateAlbum: UpdateReleaseMbzID: %w", err)
|
||||
}
|
||||
}
|
||||
if opts.Image != uuid.Nil {
|
||||
|
|
@ -197,7 +200,7 @@ func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error {
|
|||
ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateAlbum: UpdateReleaseImage: %w", err)
|
||||
}
|
||||
}
|
||||
if opts.VariousArtistsUpdate {
|
||||
|
|
@ -207,7 +210,7 @@ func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error {
|
|||
VariousArtists: opts.VariousArtistsValue,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateAlbum: UpdateReleaseVariousArtists: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -221,13 +224,13 @@ func (d *Psql) SaveAlbumAliases(ctx context.Context, id int32, aliases []string,
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SaveAlbumAliases: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
existing, err := qtx.GetAllReleaseAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveAlbumAliases: GetAllReleaseAliases: %w", err)
|
||||
}
|
||||
for _, v := range existing {
|
||||
aliases = append(aliases, v.Alias)
|
||||
|
|
@ -235,7 +238,7 @@ func (d *Psql) SaveAlbumAliases(ctx context.Context, id int32, aliases []string,
|
|||
utils.Unique(&aliases)
|
||||
for _, alias := range aliases {
|
||||
if strings.TrimSpace(alias) == "" {
|
||||
return errors.New("aliases cannot be blank")
|
||||
return errors.New("SaveAlbumAliases: aliases cannot be blank")
|
||||
}
|
||||
err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{
|
||||
Alias: strings.TrimSpace(alias),
|
||||
|
|
@ -244,7 +247,7 @@ func (d *Psql) SaveAlbumAliases(ctx context.Context, id int32, aliases []string,
|
|||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveAlbumAliases: InsertReleaseAlias: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -263,7 +266,7 @@ func (d *Psql) DeleteAlbumAlias(ctx context.Context, id int32, alias string) err
|
|||
func (d *Psql) GetAllAlbumAliases(ctx context.Context, id int32) ([]models.Alias, error) {
|
||||
rows, err := d.q.GetAllReleaseAliases(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAllAlbumAliases: GetAllReleaseAliases: %w", err)
|
||||
}
|
||||
aliases := make([]models.Alias, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -285,14 +288,14 @@ func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string)
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryAlbumAlias: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
// get all aliases
|
||||
aliases, err := qtx.GetAllReleaseAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryAlbumAlias: GetAllReleaseAliases: %w", err)
|
||||
}
|
||||
primary := ""
|
||||
exists := false
|
||||
|
|
@ -309,7 +312,7 @@ func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string)
|
|||
return nil
|
||||
}
|
||||
if !exists {
|
||||
return errors.New("alias does not exist")
|
||||
return errors.New("SetPrimaryAlbumAlias: alias does not exist")
|
||||
}
|
||||
err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
|
||||
ReleaseID: id,
|
||||
|
|
@ -317,7 +320,7 @@ func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string)
|
|||
IsPrimary: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryAlbumAlias: SetReleaseAliasPrimaryStatus: %w", err)
|
||||
}
|
||||
err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
|
||||
ReleaseID: id,
|
||||
|
|
@ -325,7 +328,61 @@ func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string)
|
|||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryAlbumAlias: SetReleaseAliasPrimaryStatus: %w", err)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (d *Psql) SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if id == 0 {
|
||||
return errors.New("artist id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return fmt.Errorf("SetPrimaryAlbumArtist: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
// get all artists
|
||||
artists, err := qtx.GetReleaseArtists(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryAlbumArtist: GetReleaseArtists: %w", err)
|
||||
}
|
||||
var primary int32
|
||||
for _, v := range artists {
|
||||
// i dont get it??? is_primary is not a nullable column??? why use pgtype.Bool???
|
||||
// why not just use boolean??? is sqlc stupid??? am i stupid???????
|
||||
if v.IsPrimary.Valid && v.IsPrimary.Bool {
|
||||
primary = v.ID
|
||||
}
|
||||
}
|
||||
if value && primary == artistId {
|
||||
// no-op
|
||||
return nil
|
||||
}
|
||||
l.Debug().Msgf("Marking artist with id %d as 'primary = %v' on album with id %d", artistId, value, id)
|
||||
err = qtx.UpdateReleasePrimaryArtist(ctx, repository.UpdateReleasePrimaryArtistParams{
|
||||
ReleaseID: id,
|
||||
ArtistID: artistId,
|
||||
IsPrimary: value,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryAlbumArtist: UpdateReleasePrimaryArtist: %w", err)
|
||||
}
|
||||
if value && primary != 0 {
|
||||
// if we were marking a new one as primary and there was already one marked as primary,
|
||||
// unmark that one as there can only be one
|
||||
l.Debug().Msgf("Unmarking artist with id %d as primary on album with id %d", primary, id)
|
||||
err = qtx.UpdateReleasePrimaryArtist(ctx, repository.UpdateReleasePrimaryArtistParams{
|
||||
ReleaseID: id,
|
||||
ArtistID: primary,
|
||||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryAlbumArtist: UpdateReleasePrimaryArtist: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
l.Debug().Msgf("Fetching artist from DB with id %d", opts.ID)
|
||||
row, err := d.q.GetArtist(ctx, opts.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: GetArtist by ID: %w", err)
|
||||
}
|
||||
count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{
|
||||
ListenedAt: time.Unix(0, 0),
|
||||
|
|
@ -31,14 +32,14 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
}
|
||||
return &models.Artist{
|
||||
ID: row.ID,
|
||||
|
|
@ -53,7 +54,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
l.Debug().Msgf("Fetching artist from DB with MusicBrainz ID %s", opts.MusicBrainzID)
|
||||
row, err := d.q.GetArtistByMbzID(ctx, &opts.MusicBrainzID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: GetArtistByMbzID: %w", err)
|
||||
}
|
||||
count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{
|
||||
ListenedAt: time.Unix(0, 0),
|
||||
|
|
@ -61,14 +62,14 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
}
|
||||
return &models.Artist{
|
||||
ID: row.ID,
|
||||
|
|
@ -83,7 +84,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
l.Debug().Msgf("Fetching artist from DB with name '%s'", opts.Name)
|
||||
row, err := d.q.GetArtistByName(ctx, opts.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: GetArtistByName: %w", err)
|
||||
}
|
||||
count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{
|
||||
ListenedAt: time.Unix(0, 0),
|
||||
|
|
@ -91,14 +92,14 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err)
|
||||
}
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
Period: db.PeriodAllTime,
|
||||
ArtistID: row.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
||||
}
|
||||
return &models.Artist{
|
||||
ID: row.ID,
|
||||
|
|
@ -118,35 +119,36 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
|||
func (d *Psql) SaveArtistAliases(ctx context.Context, id int32, aliases []string, source string) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if id == 0 {
|
||||
return errors.New("artist id not specified")
|
||||
return errors.New("SaveArtistAliases: artist id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SaveArtistAliases: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
existing, err := qtx.GetAllArtistAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveArtistAliases: GetAllArtistAliases: %w", err)
|
||||
}
|
||||
for _, v := range existing {
|
||||
aliases = append(aliases, v.Alias)
|
||||
}
|
||||
utils.Unique(&aliases)
|
||||
for _, alias := range aliases {
|
||||
if strings.TrimSpace(alias) == "" {
|
||||
return errors.New("aliases cannot be blank")
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
return errors.New("SaveArtistAliases: aliases cannot be blank")
|
||||
}
|
||||
err = qtx.InsertArtistAlias(ctx, repository.InsertArtistAliasParams{
|
||||
Alias: strings.TrimSpace(alias),
|
||||
Alias: alias,
|
||||
ArtistID: id,
|
||||
Source: source,
|
||||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveArtistAliases: InsertArtistAlias: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -170,13 +172,13 @@ func (d *Psql) SaveArtist(ctx context.Context, opts db.SaveArtistOpts) (*models.
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveArtist: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
opts.Name = strings.TrimSpace(opts.Name)
|
||||
if opts.Name == "" {
|
||||
return nil, errors.New("name must not be blank")
|
||||
return nil, errors.New("SaveArtist: name must not be blank")
|
||||
}
|
||||
l.Debug().Msgf("Inserting artist '%s' into DB", opts.Name)
|
||||
a, err := qtx.InsertArtist(ctx, repository.InsertArtistParams{
|
||||
|
|
@ -185,7 +187,7 @@ func (d *Psql) SaveArtist(ctx context.Context, opts db.SaveArtistOpts) (*models.
|
|||
ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveArtist: InsertArtist: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Inserting canonical alias '%s' into DB for artist with id %d", opts.Name, a.ID)
|
||||
err = qtx.InsertArtistAlias(ctx, repository.InsertArtistAliasParams{
|
||||
|
|
@ -195,13 +197,13 @@ func (d *Psql) SaveArtist(ctx context.Context, opts db.SaveArtistOpts) (*models.
|
|||
IsPrimary: true,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msgf("Error inserting canonical alias for artist '%s'", opts.Name)
|
||||
return nil, err
|
||||
l.Err(err).Msgf("SaveArtist: error inserting canonical alias for artist '%s'", opts.Name)
|
||||
return nil, fmt.Errorf("SaveArtist: InsertArtistAlias: %w", err)
|
||||
}
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to commit insert artist transaction")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveArtist: Commit: %w", err)
|
||||
}
|
||||
artist := &models.Artist{
|
||||
ID: a.ID,
|
||||
|
|
@ -214,7 +216,7 @@ func (d *Psql) SaveArtist(ctx context.Context, opts db.SaveArtistOpts) (*models.
|
|||
l.Debug().Msgf("Inserting aliases '%v' into DB for artist '%s'", opts.Aliases, opts.Name)
|
||||
err = d.SaveArtistAliases(ctx, a.ID, opts.Aliases, "MusicBrainz")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveArtist: SaveArtistAliases: %w", err)
|
||||
}
|
||||
artist.Aliases = opts.Aliases
|
||||
}
|
||||
|
|
@ -224,12 +226,12 @@ func (d *Psql) SaveArtist(ctx context.Context, opts db.SaveArtistOpts) (*models.
|
|||
func (d *Psql) UpdateArtist(ctx context.Context, opts db.UpdateArtistOpts) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if opts.ID == 0 {
|
||||
return errors.New("artist id not specified")
|
||||
return errors.New("UpdateArtist: artist id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("UpdateArtist: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -240,7 +242,7 @@ func (d *Psql) UpdateArtist(ctx context.Context, opts db.UpdateArtistOpts) error
|
|||
MusicBrainzID: &opts.MusicBrainzID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateArtist: UpdateArtistMbzID: %w", err)
|
||||
}
|
||||
}
|
||||
if opts.Image != uuid.Nil {
|
||||
|
|
@ -251,10 +253,15 @@ func (d *Psql) UpdateArtist(ctx context.Context, opts db.UpdateArtistOpts) error
|
|||
ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateArtist: UpdateArtistImage: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to commit update artist transaction")
|
||||
return fmt.Errorf("UpdateArtist: Commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Psql) DeleteArtistAlias(ctx context.Context, id int32, alias string) error {
|
||||
|
|
@ -263,10 +270,11 @@ func (d *Psql) DeleteArtistAlias(ctx context.Context, id int32, alias string) er
|
|||
Alias: alias,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Psql) GetAllArtistAliases(ctx context.Context, id int32) ([]models.Alias, error) {
|
||||
rows, err := d.q.GetAllArtistAliases(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAllArtistAliases: %w", err)
|
||||
}
|
||||
aliases := make([]models.Alias, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -283,19 +291,18 @@ func (d *Psql) GetAllArtistAliases(ctx context.Context, id int32) ([]models.Alia
|
|||
func (d *Psql) SetPrimaryArtistAlias(ctx context.Context, id int32, alias string) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if id == 0 {
|
||||
return errors.New("artist id not specified")
|
||||
return errors.New("SetPrimaryArtistAlias: artist id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryArtistAlias: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
// get all aliases
|
||||
aliases, err := qtx.GetAllArtistAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryArtistAlias: GetAllArtistAliases: %w", err)
|
||||
}
|
||||
primary := ""
|
||||
exists := false
|
||||
|
|
@ -308,11 +315,10 @@ func (d *Psql) SetPrimaryArtistAlias(ctx context.Context, id int32, alias string
|
|||
}
|
||||
}
|
||||
if primary == alias {
|
||||
// no-op rename
|
||||
return nil
|
||||
}
|
||||
if !exists {
|
||||
return errors.New("alias does not exist")
|
||||
return errors.New("SetPrimaryArtistAlias: alias does not exist")
|
||||
}
|
||||
err = qtx.SetArtistAliasPrimaryStatus(ctx, repository.SetArtistAliasPrimaryStatusParams{
|
||||
ArtistID: id,
|
||||
|
|
@ -320,7 +326,7 @@ func (d *Psql) SetPrimaryArtistAlias(ctx context.Context, id int32, alias string
|
|||
IsPrimary: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryArtistAlias: SetArtistAliasPrimaryStatus (primary): %w", err)
|
||||
}
|
||||
err = qtx.SetArtistAliasPrimaryStatus(ctx, repository.SetArtistAliasPrimaryStatusParams{
|
||||
ArtistID: id,
|
||||
|
|
@ -328,7 +334,57 @@ func (d *Psql) SetPrimaryArtistAlias(ctx context.Context, id int32, alias string
|
|||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryArtistAlias: SetArtistAliasPrimaryStatus (previous primary): %w", err)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to commit transaction")
|
||||
return fmt.Errorf("SetPrimaryArtistAlias: Commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (d *Psql) GetArtistsForAlbum(ctx context.Context, id int32) ([]*models.Artist, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
l.Debug().Msgf("Fetching artists for album ID %d", id)
|
||||
|
||||
rows, err := d.q.GetReleaseArtists(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetArtistsForAlbum: %w", err)
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(rows))
|
||||
for i, row := range rows {
|
||||
artists[i] = &models.Artist{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
MbzID: row.MusicBrainzID,
|
||||
Image: row.Image,
|
||||
IsPrimary: row.IsPrimary.Valid && row.IsPrimary.Bool,
|
||||
}
|
||||
}
|
||||
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
func (d *Psql) GetArtistsForTrack(ctx context.Context, id int32) ([]*models.Artist, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
l.Debug().Msgf("Fetching artists for track ID %d", id)
|
||||
|
||||
rows, err := d.q.GetTrackArtists(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetArtistsForTrack: %w", err)
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(rows))
|
||||
for i, row := range rows {
|
||||
artists[i] = &models.Artist{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
MbzID: row.MusicBrainzID,
|
||||
Image: row.Image,
|
||||
IsPrimary: row.IsPrimary.Valid && row.IsPrimary.Bool,
|
||||
}
|
||||
}
|
||||
|
||||
return artists, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
|
|
@ -17,10 +18,11 @@ func (p *Psql) CountListens(ctx context.Context, period db.Period) (int64, error
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountListens: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountTracks(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
|
|
@ -29,10 +31,11 @@ func (p *Psql) CountTracks(ctx context.Context, period db.Period) (int64, error)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountTracks: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountAlbums(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
|
|
@ -41,10 +44,11 @@ func (p *Psql) CountAlbums(ctx context.Context, period db.Period) (int64, error)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountAlbums: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountArtists(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
|
|
@ -53,10 +57,11 @@ func (p *Psql) CountArtists(ctx context.Context, period db.Period) (int64, error
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountArtists: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountTimeListened(ctx context.Context, period db.Period) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(period)
|
||||
|
|
@ -65,10 +70,11 @@ func (p *Psql) CountTimeListened(ctx context.Context, period db.Period) (int64,
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountTimeListened: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListenedOpts) (int64, error) {
|
||||
t2 := time.Now()
|
||||
t1 := db.StartTimeFromPeriod(opts.Period)
|
||||
|
|
@ -80,7 +86,7 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
|
|||
ArtistID: opts.ArtistID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountTimeListenedToItem (Artist): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
} else if opts.AlbumID > 0 {
|
||||
|
|
@ -90,10 +96,9 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
|
|||
ReleaseID: opts.AlbumID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountTimeListenedToItem (Album): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
|
||||
} else if opts.TrackID > 0 {
|
||||
count, err := p.q.CountTimeListenedToTrack(ctx, repository.CountTimeListenedToTrackParams{
|
||||
ListenedAt: t1,
|
||||
|
|
@ -101,9 +106,9 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
|
|||
ID: opts.TrackID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("CountTimeListenedToItem (Track): %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
return 0, errors.New("an id must be provided")
|
||||
return 0, errors.New("CountTimeListenedToItem: an id must be provided")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
"github.com/gabehf/koito/internal/models"
|
||||
|
|
@ -15,15 +16,15 @@ import (
|
|||
func (d *Psql) ImageHasAssociation(ctx context.Context, image uuid.UUID) (bool, error) {
|
||||
_, err := d.q.GetReleaseByImageID(ctx, &image)
|
||||
if err == nil {
|
||||
return true, err
|
||||
return true, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, err
|
||||
return false, fmt.Errorf("ImageHasAssociation: GetReleaseByImageID: %w", err)
|
||||
}
|
||||
_, err = d.q.GetArtistByImage(ctx, &image)
|
||||
if err == nil {
|
||||
return true, err
|
||||
return true, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, err
|
||||
return false, fmt.Errorf("ImageHasAssociation: GetArtistByImage: %w", err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -31,15 +32,15 @@ func (d *Psql) ImageHasAssociation(ctx context.Context, image uuid.UUID) (bool,
|
|||
func (d *Psql) GetImageSource(ctx context.Context, image uuid.UUID) (string, error) {
|
||||
r, err := d.q.GetReleaseByImageID(ctx, &image)
|
||||
if err == nil {
|
||||
return r.ImageSource.String, err
|
||||
return r.ImageSource.String, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetImageSource: GetReleaseByImageID: %w", err)
|
||||
}
|
||||
rr, err := d.q.GetArtistByImage(ctx, &image)
|
||||
if err == nil {
|
||||
return rr.ImageSource.String, err
|
||||
return rr.ImageSource.String, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetImageSource: GetArtistByImage: %w", err)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -51,14 +52,13 @@ func (d *Psql) AlbumsWithoutImages(ctx context.Context, from int32) ([]*models.A
|
|||
ID: from,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("AlbumsWithoutImages: GetReleasesWithoutImages: %w", err)
|
||||
}
|
||||
albums := make([]*models.Album, len(rows))
|
||||
for i, row := range rows {
|
||||
artists := make([]models.SimpleArtist, 0)
|
||||
err = json.Unmarshal(row.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", row.ID)
|
||||
var artists []models.SimpleArtist
|
||||
if err := json.Unmarshal(row.Artists, &artists); err != nil {
|
||||
l.Err(err).Msgf("AlbumsWithoutImages: error unmarshalling artists for release group with id %d", row.ID)
|
||||
artists = nil
|
||||
}
|
||||
albums[i] = &models.Album{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
|
|
@ -18,7 +19,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
offset := (opts.Page - 1) * opts.Limit
|
||||
t1, t2, err := utils.DateRange(opts.Week, opts.Month, opts.Year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: %w", err)
|
||||
}
|
||||
if opts.Month == 0 && opts.Year == 0 {
|
||||
// use period, not date range
|
||||
|
|
@ -41,7 +42,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ID: int32(opts.TrackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: GetLastListensFromTrackPaginated: %w", err)
|
||||
}
|
||||
listens = make([]*models.Listen, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -54,7 +55,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &t.Track.Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
listens[i] = t
|
||||
}
|
||||
|
|
@ -64,7 +65,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
TrackID: int32(opts.TrackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: CountListensFromTrack: %w", err)
|
||||
}
|
||||
} else if opts.AlbumID > 0 {
|
||||
l.Debug().Msgf("Fetching %d listens with period %s on page %d from range %v to %v",
|
||||
|
|
@ -77,7 +78,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ReleaseID: int32(opts.AlbumID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: GetLastListensFromReleasePaginated: %w", err)
|
||||
}
|
||||
listens = make([]*models.Listen, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -90,7 +91,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &t.Track.Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
listens[i] = t
|
||||
}
|
||||
|
|
@ -100,7 +101,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ReleaseID: int32(opts.AlbumID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: CountListensFromRelease: %w", err)
|
||||
}
|
||||
} else if opts.ArtistID > 0 {
|
||||
l.Debug().Msgf("Fetching %d listens with period %s on page %d from range %v to %v",
|
||||
|
|
@ -113,7 +114,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ArtistID: int32(opts.ArtistID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: GetLastListensFromArtistPaginated: %w", err)
|
||||
}
|
||||
listens = make([]*models.Listen, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -126,7 +127,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &t.Track.Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
listens[i] = t
|
||||
}
|
||||
|
|
@ -136,7 +137,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ArtistID: int32(opts.ArtistID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: CountListensFromArtist: %w", err)
|
||||
}
|
||||
} else {
|
||||
l.Debug().Msgf("Fetching %d listens with period %s on page %d from range %v to %v",
|
||||
|
|
@ -148,7 +149,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: GetLastListensPaginated: %w", err)
|
||||
}
|
||||
listens = make([]*models.Listen, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -161,7 +162,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &t.Track.Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
listens[i] = t
|
||||
}
|
||||
|
|
@ -170,7 +171,7 @@ func (d *Psql) GetListensPaginated(ctx context.Context, opts db.GetItemsOpts) (*
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListensPaginated: CountListens: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Database responded with %d tracks out of a total %d", len(rows), count)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
|
|
@ -30,7 +31,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
|
|||
ReleaseID: opts.AlbumID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListenActivity: ListenActivityForRelease: %w", err)
|
||||
}
|
||||
listenActivity = make([]db.ListenActivityItem, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -51,7 +52,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
|
|||
ArtistID: opts.ArtistID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListenActivity: ListenActivityForArtist: %w", err)
|
||||
}
|
||||
listenActivity = make([]db.ListenActivityItem, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -72,7 +73,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
|
|||
ID: opts.TrackID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListenActivity: ListenActivityForTrack: %w", err)
|
||||
}
|
||||
listenActivity = make([]db.ListenActivityItem, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -92,7 +93,7 @@ func (d *Psql) GetListenActivity(ctx context.Context, opts db.ListenActivityOpts
|
|||
Column3: stepToInterval(opts.Step),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetListenActivity: ListenActivity: %w", err)
|
||||
}
|
||||
listenActivity = make([]db.ListenActivityItem, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func (d *Psql) MergeAlbums(ctx context.Context, fromId, toId int32, replaceImage
|
|||
|
||||
fromArtists, err := qtx.GetReleaseArtists(ctx, fromId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MergeTracks: GetReleaseArtists: %w", err)
|
||||
return fmt.Errorf("MergeAlbums: GetReleaseArtists: %w", err)
|
||||
}
|
||||
|
||||
err = qtx.UpdateReleaseForAll(ctx, repository.UpdateReleaseForAllParams{
|
||||
|
|
|
|||
|
|
@ -34,34 +34,34 @@ func New() (*Psql, error) {
|
|||
|
||||
config, err := pgxpool.ParseConfig(cfg.DatabaseUrl())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse pgx config: %w", err)
|
||||
return nil, fmt.Errorf("psql.New: failed to parse pgx config: %w", err)
|
||||
}
|
||||
|
||||
config.ConnConfig.ConnectTimeout = 15 * time.Second
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pgx pool: %w", err)
|
||||
return nil, fmt.Errorf("psql.New: failed to create pgx pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("database not reachable: %w", err)
|
||||
return nil, fmt.Errorf("psql.New: database not reachable: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := sql.Open("pgx", cfg.DatabaseUrl())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open db for migrations: %w", err)
|
||||
return nil, fmt.Errorf("psql.New: failed to open db for migrations: %w", err)
|
||||
}
|
||||
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get caller info")
|
||||
return nil, fmt.Errorf("psql.New: unable to get caller info")
|
||||
}
|
||||
migrationsPath := filepath.Join(filepath.Dir(filename), "..", "..", "..", "db", "migrations")
|
||||
|
||||
if err := goose.Up(sqlDB, migrationsPath); err != nil {
|
||||
return nil, fmt.Errorf("goose failed: %w", err)
|
||||
return nil, fmt.Errorf("psql.New: goose failed: %w", err)
|
||||
}
|
||||
_ = sqlDB.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gabehf/koito/internal/models"
|
||||
"github.com/gabehf/koito/internal/repository"
|
||||
|
|
@ -19,7 +20,7 @@ func (d *Psql) SearchArtists(ctx context.Context, q string) ([]*models.Artist, e
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchArtist: SearchArtistsBySubstring: %w", err)
|
||||
}
|
||||
ret := make([]*models.Artist, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -37,7 +38,7 @@ func (d *Psql) SearchArtists(ctx context.Context, q string) ([]*models.Artist, e
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchArtist: SearchArtists: %w", err)
|
||||
}
|
||||
ret := make([]*models.Artist, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -59,7 +60,7 @@ func (d *Psql) SearchAlbums(ctx context.Context, q string) ([]*models.Album, err
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchAlbums: SearchReleasesBySubstring: %w", err)
|
||||
}
|
||||
ret := make([]*models.Album, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -72,7 +73,7 @@ func (d *Psql) SearchAlbums(ctx context.Context, q string) ([]*models.Album, err
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &ret[i].Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchAlbums: Unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
|
|
@ -82,7 +83,7 @@ func (d *Psql) SearchAlbums(ctx context.Context, q string) ([]*models.Album, err
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchAlbums: SearchReleases: %w", err)
|
||||
}
|
||||
ret := make([]*models.Album, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -95,7 +96,7 @@ func (d *Psql) SearchAlbums(ctx context.Context, q string) ([]*models.Album, err
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &ret[i].Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchAlbums: Unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
|
|
@ -109,7 +110,7 @@ func (d *Psql) SearchTracks(ctx context.Context, q string) ([]*models.Track, err
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchTracks: SearchTracksBySubstring: %w", err)
|
||||
}
|
||||
ret := make([]*models.Track, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -121,7 +122,7 @@ func (d *Psql) SearchTracks(ctx context.Context, q string) ([]*models.Track, err
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &ret[i].Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchTracks: Unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
|
|
@ -131,7 +132,7 @@ func (d *Psql) SearchTracks(ctx context.Context, q string) ([]*models.Track, err
|
|||
Limit: searchItemLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchTracks: SearchTracks: %w", err)
|
||||
}
|
||||
ret := make([]*models.Track, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -143,7 +144,7 @@ func (d *Psql) SearchTracks(ctx context.Context, q string) ([]*models.Track, err
|
|||
}
|
||||
err = json.Unmarshal(row.Artists, &ret[i].Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SearchTracks: Unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/models"
|
||||
|
|
@ -19,7 +20,7 @@ func (d *Psql) SaveSession(ctx context.Context, userID int32, expiresAt time.Tim
|
|||
Persistent: persistent,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveSession: InsertSession: %w", err)
|
||||
}
|
||||
return &models.Session{
|
||||
ID: session.ID,
|
||||
|
|
@ -47,7 +48,7 @@ func (d *Psql) GetUserBySession(ctx context.Context, sessionId uuid.UUID) (*mode
|
|||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveSession: GetUserBySession: %w", err)
|
||||
}
|
||||
|
||||
return &models.User{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
|
|
@ -17,7 +18,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
offset := (opts.Page - 1) * opts.Limit
|
||||
t1, t2, err := utils.DateRange(opts.Week, opts.Month, opts.Year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: %w", err)
|
||||
}
|
||||
if opts.Month == 0 && opts.Year == 0 {
|
||||
// use period, not date range
|
||||
|
|
@ -43,7 +44,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesFromArtist: %w", err)
|
||||
}
|
||||
rgs = make([]*models.Album, len(rows))
|
||||
l.Debug().Msgf("Database responded with %d items", len(rows))
|
||||
|
|
@ -52,7 +53,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
err = json.Unmarshal(v.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", v.ID)
|
||||
artists = nil
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
rgs[i] = &models.Album{
|
||||
ID: v.ID,
|
||||
|
|
@ -66,7 +67,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
}
|
||||
count, err = d.q.CountReleasesFromArtist(ctx, int32(opts.ArtistID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: CountReleasesFromArtist: %w", err)
|
||||
}
|
||||
} else {
|
||||
l.Debug().Msgf("Fetching top %d albums with period %s on page %d from range %v to %v",
|
||||
|
|
@ -78,7 +79,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesPaginated: %w", err)
|
||||
}
|
||||
rgs = make([]*models.Album, len(rows))
|
||||
l.Debug().Msgf("Database responded with %d items", len(rows))
|
||||
|
|
@ -87,7 +88,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
err = json.Unmarshal(row.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", row.ID)
|
||||
artists = nil
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
t := &models.Album{
|
||||
Title: row.Title,
|
||||
|
|
@ -105,7 +106,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopAlbumsPaginated: CountTopReleases: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Database responded with %d albums out of a total %d", len(rows), count)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package psql
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
|
|
@ -16,7 +17,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
offset := (opts.Page - 1) * opts.Limit
|
||||
t1, t2, err := utils.DateRange(opts.Week, opts.Month, opts.Year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopArtistsPaginated: %w", err)
|
||||
}
|
||||
if opts.Month == 0 && opts.Year == 0 {
|
||||
// use period, not date range
|
||||
|
|
@ -35,7 +36,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopArtistsPaginated: GetTopArtistsPaginated: %w", err)
|
||||
}
|
||||
rgs := make([]*models.Artist, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -53,7 +54,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopArtistsPaginated: CountTopArtists: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Database responded with %d artists out of a total %d", len(rows), count)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gabehf/koito/internal/db"
|
||||
|
|
@ -17,7 +18,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
offset := (opts.Page - 1) * opts.Limit
|
||||
t1, t2, err := utils.DateRange(opts.Week, opts.Month, opts.Year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: %w", err)
|
||||
}
|
||||
if opts.Month == 0 && opts.Year == 0 {
|
||||
// use period, not date range
|
||||
|
|
@ -40,7 +41,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ReleaseID: int32(opts.AlbumID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksInReleasePaginated: %w", err)
|
||||
}
|
||||
tracks = make([]*models.Track, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -48,7 +49,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
err = json.Unmarshal(row.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for track with id %d", row.ID)
|
||||
artists = nil
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
t := &models.Track{
|
||||
Title: row.Title,
|
||||
|
|
@ -80,7 +81,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ArtistID: int32(opts.ArtistID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksByArtistPaginated: %w", err)
|
||||
}
|
||||
tracks = make([]*models.Track, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -88,7 +89,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
err = json.Unmarshal(row.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for track with id %d", row.ID)
|
||||
artists = nil
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
t := &models.Track{
|
||||
Title: row.Title,
|
||||
|
|
@ -107,7 +108,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ArtistID: int32(opts.ArtistID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: CountTopTracksByArtist: %w", err)
|
||||
}
|
||||
} else {
|
||||
l.Debug().Msgf("Fetching top %d tracks with period %s on page %d from range %v to %v",
|
||||
|
|
@ -119,7 +120,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksPaginated: %w", err)
|
||||
}
|
||||
tracks = make([]*models.Track, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -127,7 +128,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
err = json.Unmarshal(row.Artists, &artists)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Error unmarshalling artists for track with id %d", row.ID)
|
||||
artists = nil
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: Unmarshal: %w", err)
|
||||
}
|
||||
t := &models.Track{
|
||||
Title: row.Title,
|
||||
|
|
@ -145,7 +146,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
|
|||
ListenedAt_2: t2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTopTracksPaginated: CountTopTracks: %w", err)
|
||||
}
|
||||
l.Debug().Msgf("Database responded with %d tracks out of a total %d", len(rows), count)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
l.Debug().Msgf("Fetching track from DB with id %d", opts.ID)
|
||||
t, err := d.q.GetTrack(ctx, opts.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: GetTrack By ID: %w", err)
|
||||
}
|
||||
track = models.Track{
|
||||
ID: t.ID,
|
||||
|
|
@ -37,7 +38,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
l.Debug().Msgf("Fetching track from DB with MusicBrainz ID %s", opts.MusicBrainzID)
|
||||
t, err := d.q.GetTrackByMbzID(ctx, &opts.MusicBrainzID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: GetTrackByMbzID: %w", err)
|
||||
}
|
||||
track = models.Track{
|
||||
ID: t.ID,
|
||||
|
|
@ -53,7 +54,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
Column2: opts.ArtistIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: GetTrackByTitleAndArtists: %w", err)
|
||||
}
|
||||
track = models.Track{
|
||||
ID: t.ID,
|
||||
|
|
@ -63,7 +64,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
Duration: t.Duration,
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("insufficient information to get track")
|
||||
return nil, errors.New("GetTrack: insufficient information to get track")
|
||||
}
|
||||
|
||||
count, err := d.q.CountListensFromTrack(ctx, repository.CountListensFromTrackParams{
|
||||
|
|
@ -72,7 +73,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
TrackID: track.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: CountListensFromTrack: %w", err)
|
||||
}
|
||||
|
||||
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
||||
|
|
@ -80,7 +81,7 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
|||
TrackID: track.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err)
|
||||
}
|
||||
|
||||
track.ListenCount = count
|
||||
|
|
@ -97,20 +98,20 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
|
|||
insertMbzID = &opts.RecordingMbzID
|
||||
}
|
||||
if len(opts.ArtistIDs) < 1 {
|
||||
return nil, errors.New("required parameter 'ArtistIDs' missing")
|
||||
return nil, errors.New("SaveTrack: required parameter 'ArtistIDs' missing")
|
||||
}
|
||||
for _, aid := range opts.ArtistIDs {
|
||||
if aid == 0 {
|
||||
return nil, errors.New("none of 'ArtistIDs' may be 0")
|
||||
return nil, errors.New("SaveTrack: none of 'ArtistIDs' may be 0")
|
||||
}
|
||||
}
|
||||
if opts.AlbumID == 0 {
|
||||
return nil, errors.New("required parameter 'AlbumID' missing")
|
||||
return nil, errors.New("SaveTrack: required parameter 'AlbumID' missing")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveTrack: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -120,7 +121,7 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
|
|||
ReleaseID: opts.AlbumID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveTrack: InsertTrack: %w", err)
|
||||
}
|
||||
// insert associated artists
|
||||
for _, aid := range opts.ArtistIDs {
|
||||
|
|
@ -129,7 +130,7 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
|
|||
TrackID: trackRow.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveTrack: AssociateArtistToTrack: %w", err)
|
||||
}
|
||||
}
|
||||
// insert primary alias
|
||||
|
|
@ -140,11 +141,11 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
|
|||
IsPrimary: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveTrack: InsertTrackAlias: %w", err)
|
||||
}
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveTrack: Commit: %w", err)
|
||||
}
|
||||
return &models.Track{
|
||||
ID: trackRow.ID,
|
||||
|
|
@ -156,12 +157,12 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
|
|||
func (d *Psql) UpdateTrack(ctx context.Context, opts db.UpdateTrackOpts) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if opts.ID == 0 {
|
||||
return errors.New("track id not specified")
|
||||
return errors.New("UpdateTrack: track id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("UpdateTrack: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -172,7 +173,7 @@ func (d *Psql) UpdateTrack(ctx context.Context, opts db.UpdateTrackOpts) error {
|
|||
MusicBrainzID: &opts.MusicBrainzID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateTrack: UpdateTrackMbzID: %w", err)
|
||||
}
|
||||
}
|
||||
if opts.Duration != 0 {
|
||||
|
|
@ -182,7 +183,7 @@ func (d *Psql) UpdateTrack(ctx context.Context, opts db.UpdateTrackOpts) error {
|
|||
Duration: opts.Duration,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateTrack: UpdateTrackDuration: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -191,18 +192,18 @@ func (d *Psql) UpdateTrack(ctx context.Context, opts db.UpdateTrackOpts) error {
|
|||
func (d *Psql) SaveTrackAliases(ctx context.Context, id int32, aliases []string, source string) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if id == 0 {
|
||||
return errors.New("track id not specified")
|
||||
return errors.New("SaveTrackAliases: track id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SaveTrackAliases: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
existing, err := qtx.GetAllTrackAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveTrackAliases: GetAllTrackAliases: %w", err)
|
||||
}
|
||||
for _, v := range existing {
|
||||
aliases = append(aliases, v.Alias)
|
||||
|
|
@ -219,7 +220,7 @@ func (d *Psql) SaveTrackAliases(ctx context.Context, id int32, aliases []string,
|
|||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SaveTrackAliases: InsertTrackAlias: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -239,7 +240,7 @@ func (d *Psql) DeleteTrackAlias(ctx context.Context, id int32, alias string) err
|
|||
func (d *Psql) GetAllTrackAliases(ctx context.Context, id int32) ([]models.Alias, error) {
|
||||
rows, err := d.q.GetAllTrackAliases(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetAllTrackAliases: GetAllTrackAliases: %w", err)
|
||||
}
|
||||
aliases := make([]models.Alias, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
@ -261,14 +262,14 @@ func (d *Psql) SetPrimaryTrackAlias(ctx context.Context, id int32, alias string)
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryTrackAlias: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
// get all aliases
|
||||
aliases, err := qtx.GetAllTrackAliases(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryTrackAlias: GetAllTrackAliases: %w", err)
|
||||
}
|
||||
primary := ""
|
||||
exists := false
|
||||
|
|
@ -293,7 +294,7 @@ func (d *Psql) SetPrimaryTrackAlias(ctx context.Context, id int32, alias string)
|
|||
IsPrimary: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryTrackAlias: SetTrackAliasPrimaryStatus: %w", err)
|
||||
}
|
||||
err = qtx.SetTrackAliasPrimaryStatus(ctx, repository.SetTrackAliasPrimaryStatusParams{
|
||||
TrackID: id,
|
||||
|
|
@ -301,7 +302,61 @@ func (d *Psql) SetPrimaryTrackAlias(ctx context.Context, id int32, alias string)
|
|||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("SetPrimaryTrackAlias: SetTrackAliasPrimaryStatus: %w", err)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (d *Psql) SetPrimaryTrackArtist(ctx context.Context, id int32, artistId int32, value bool) error {
|
||||
l := logger.FromContext(ctx)
|
||||
if id == 0 {
|
||||
return errors.New("artist id not specified")
|
||||
}
|
||||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return fmt.Errorf("SetPrimaryTrackArtist: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
// get all artists
|
||||
artists, err := qtx.GetTrackArtists(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryTrackArtist: GetTrackArtists: %w", err)
|
||||
}
|
||||
var primary int32
|
||||
for _, v := range artists {
|
||||
// i dont get it??? is_primary is not a nullable column??? why use pgtype.Bool???
|
||||
// why not just use boolean??? is sqlc stupid??? am i stupid???????
|
||||
if v.IsPrimary.Valid && v.IsPrimary.Bool {
|
||||
primary = v.ID
|
||||
}
|
||||
}
|
||||
if value && primary == artistId {
|
||||
// no-op
|
||||
return nil
|
||||
}
|
||||
l.Debug().Msgf("Marking artist with id %d as 'primary = %v' on track with id %d", artistId, value, id)
|
||||
err = qtx.UpdateTrackPrimaryArtist(ctx, repository.UpdateTrackPrimaryArtistParams{
|
||||
TrackID: id,
|
||||
ArtistID: artistId,
|
||||
IsPrimary: value,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryTrackArtist: UpdateTrackPrimaryArtist: %w", err)
|
||||
}
|
||||
if value && primary != 0 {
|
||||
l.Debug().Msgf("Unmarking artist with id %d as primary on track with id %d", primary, id)
|
||||
// if we were marking a new one as primary and there was already one marked as primary,
|
||||
// unmark that one as there can only be one
|
||||
err = qtx.UpdateTrackPrimaryArtist(ctx, repository.UpdateTrackPrimaryArtistParams{
|
||||
TrackID: id,
|
||||
ArtistID: primary,
|
||||
IsPrimary: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPrimaryTrackArtist: UpdateTrackPrimaryArtist: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package psql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
|
@ -21,7 +22,7 @@ func (d *Psql) GetUserByUsername(ctx context.Context, username string) (*models.
|
|||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetUserByUsername: %w", err)
|
||||
}
|
||||
return &models.User{
|
||||
ID: row.ID,
|
||||
|
|
@ -37,7 +38,7 @@ func (d *Psql) GetUserByApiKey(ctx context.Context, key string) (*models.User, e
|
|||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetUserByApiKey: %w", err)
|
||||
}
|
||||
return &models.User{
|
||||
ID: row.ID,
|
||||
|
|
@ -52,12 +53,12 @@ func (d *Psql) SaveUser(ctx context.Context, opts db.SaveUserOpts) (*models.User
|
|||
err := ValidateUsername(opts.Username)
|
||||
if err != nil {
|
||||
l.Debug().AnErr("validator_notice", err).Msgf("Username failed validation: %s", opts.Username)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveUser: ValidateUsername: %w", err)
|
||||
}
|
||||
pw, err := ValidateAndNormalizePassword(opts.Password)
|
||||
if err != nil {
|
||||
l.Debug().AnErr("validator_notice", err).Msgf("Password failed validation")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveUser: ValidateAndNormalizePassword: %w", err)
|
||||
}
|
||||
if opts.Role == "" {
|
||||
opts.Role = models.UserRoleUser
|
||||
|
|
@ -65,7 +66,7 @@ func (d *Psql) SaveUser(ctx context.Context, opts db.SaveUserOpts) (*models.User
|
|||
hashPw, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to generate hashed password")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveUser: bcrypt.GenerateFromPassword: %w", err)
|
||||
}
|
||||
u, err := d.q.InsertUser(ctx, repository.InsertUserParams{
|
||||
Username: strings.ToLower(opts.Username),
|
||||
|
|
@ -73,7 +74,7 @@ func (d *Psql) SaveUser(ctx context.Context, opts db.SaveUserOpts) (*models.User
|
|||
Role: repository.Role(opts.Role),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveUser: InsertUser: %w", err)
|
||||
}
|
||||
return &models.User{
|
||||
ID: u.ID,
|
||||
|
|
@ -88,7 +89,7 @@ func (d *Psql) SaveApiKey(ctx context.Context, opts db.SaveApiKeyOpts) (*models.
|
|||
UserID: opts.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("SaveApiKey: InsertApiKey: %w", err)
|
||||
}
|
||||
return &models.ApiKey{
|
||||
ID: row.ID,
|
||||
|
|
@ -107,7 +108,7 @@ func (d *Psql) UpdateUser(ctx context.Context, opts db.UpdateUserOpts) error {
|
|||
tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to begin transaction")
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: BeginTx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := d.q.WithTx(tx)
|
||||
|
|
@ -115,33 +116,33 @@ func (d *Psql) UpdateUser(ctx context.Context, opts db.UpdateUserOpts) error {
|
|||
err := ValidateUsername(opts.Username)
|
||||
if err != nil {
|
||||
l.Debug().AnErr("validator_notice", err).Msgf("Username failed validation: %s", opts.Username)
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: ValidateUsername: %w", err)
|
||||
}
|
||||
err = qtx.UpdateUserUsername(ctx, repository.UpdateUserUsernameParams{
|
||||
ID: opts.ID,
|
||||
Username: opts.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: UpdateUserUsername: %w", err)
|
||||
}
|
||||
}
|
||||
if opts.Password != "" {
|
||||
pw, err := ValidateAndNormalizePassword(opts.Password)
|
||||
if err != nil {
|
||||
l.Debug().AnErr("validator_notice", err).Msgf("Password failed validation")
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: ValidateAndNormalizePassword: %w", err)
|
||||
}
|
||||
hashPw, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to generate hashed password")
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: bcrypt.GenerateFromPassword: %w", err)
|
||||
}
|
||||
err = qtx.UpdateUserPassword(ctx, repository.UpdateUserPasswordParams{
|
||||
ID: opts.ID,
|
||||
Password: hashPw,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UpdateUser: UpdateUserPassword: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
|
@ -150,7 +151,7 @@ func (d *Psql) UpdateUser(ctx context.Context, opts db.UpdateUserOpts) error {
|
|||
func (d *Psql) GetApiKeysByUserID(ctx context.Context, id int32) ([]models.ApiKey, error) {
|
||||
rows, err := d.q.GetAllApiKeysByUserID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetApiKeysByUserID: %w", err)
|
||||
}
|
||||
keys := make([]models.ApiKey, len(rows))
|
||||
for i, row := range rows {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func NewDeezerClient() *DeezerClient {
|
|||
ret := new(DeezerClient)
|
||||
ret.url = deezerBaseUrl
|
||||
ret.userAgent = cfg.UserAgent()
|
||||
ret.requestQueue = queue.NewRequestQueue(1, 1)
|
||||
ret.requestQueue = queue.NewRequestQueue(5, 5)
|
||||
return ret
|
||||
}
|
||||
|
||||
|
|
@ -92,19 +92,19 @@ func (c *DeezerClient) getEntity(ctx context.Context, endpoint string, result an
|
|||
l.Debug().Msgf("Sending request to ImageSrc: GET %s", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
l.Debug().Msg("Adding ImageSrc request to queue")
|
||||
body, err := c.queue(ctx, req)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Deezer request failed")
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to unmarshal Deezer response")
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -121,10 +121,10 @@ func (c *DeezerClient) GetArtistImages(ctx context.Context, aliases []string) (s
|
|||
for _, a := range aliasesAscii {
|
||||
err := c.getEntity(ctx, fmt.Sprintf(artistImageEndpoint, url.QueryEscape(fmt.Sprintf("artist:\"%s\"", a))), resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetArtistImages: %w", err)
|
||||
}
|
||||
if len(resp.Data) < 1 {
|
||||
return "", errors.New("artist image not found")
|
||||
return "", errors.New("GetArtistImages: artist image not found")
|
||||
}
|
||||
for _, v := range resp.Data {
|
||||
if strings.EqualFold(v.Name, a) {
|
||||
|
|
@ -139,10 +139,10 @@ func (c *DeezerClient) GetArtistImages(ctx context.Context, aliases []string) (s
|
|||
for _, a := range utils.RemoveInBoth(aliasesUniq, aliasesAscii) {
|
||||
err := c.getEntity(ctx, fmt.Sprintf(artistImageEndpoint, url.QueryEscape(fmt.Sprintf("artist:\"%s\"", a))), resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetArtistImages: %w", err)
|
||||
}
|
||||
if len(resp.Data) < 1 {
|
||||
return "", errors.New("artist image not found")
|
||||
return "", errors.New("GetArtistImages: artist image not found")
|
||||
}
|
||||
for _, v := range resp.Data {
|
||||
if strings.EqualFold(v.Name, a) {
|
||||
|
|
@ -152,7 +152,7 @@ func (c *DeezerClient) GetArtistImages(ctx context.Context, aliases []string) (s
|
|||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("artist image not found")
|
||||
return "", errors.New("GetArtistImages: artist image not found")
|
||||
}
|
||||
|
||||
func (c *DeezerClient) GetAlbumImages(ctx context.Context, artists []string, album string) (string, error) {
|
||||
|
|
@ -163,7 +163,7 @@ func (c *DeezerClient) GetAlbumImages(ctx context.Context, artists []string, alb
|
|||
for _, alias := range artists {
|
||||
err := c.getEntity(ctx, fmt.Sprintf(albumImageEndpoint, url.QueryEscape(fmt.Sprintf("artist:\"%s\"album:\"%s\"", alias, album))), resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetAlbumImages: %w", err)
|
||||
}
|
||||
if len(resp.Data) > 0 {
|
||||
for _, v := range resp.Data {
|
||||
|
|
@ -179,7 +179,7 @@ func (c *DeezerClient) GetAlbumImages(ctx context.Context, artists []string, alb
|
|||
// if none are found, try to find an album just by album title
|
||||
err := c.getEntity(ctx, fmt.Sprintf(albumImageEndpoint, url.QueryEscape(fmt.Sprintf("album:\"%s\"", album))), resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GetAlbumImages: %w", err)
|
||||
}
|
||||
for _, v := range resp.Data {
|
||||
if strings.EqualFold(v.Title, album) {
|
||||
|
|
@ -189,5 +189,5 @@ func (c *DeezerClient) GetAlbumImages(ctx context.Context, artists []string, alb
|
|||
}
|
||||
}
|
||||
|
||||
return "", errors.New("album image not found")
|
||||
return "", errors.New("GetAlbumImages: album image not found")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
|
|||
}
|
||||
return img, nil
|
||||
}
|
||||
l.Warn().Msg("No image providers are enabled")
|
||||
l.Warn().Msg("GetArtistImage: No image providers are enabled")
|
||||
return "", nil
|
||||
}
|
||||
func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
||||
|
|
@ -102,6 +102,6 @@ func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
|||
}
|
||||
return img, nil
|
||||
}
|
||||
l.Warn().Msg("No image providers are enabled")
|
||||
l.Warn().Msg("GetAlbumImage: No image providers are enabled")
|
||||
return "", nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package importer
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
|
@ -46,7 +47,7 @@ func ImportLastFMFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrainzCall
|
|||
file, err := os.Open(path.Join(cfg.ConfigDir(), "import", filename))
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Failed to read import file: %s", filename)
|
||||
return err
|
||||
return fmt.Errorf("ImportLastFMFile: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
var throttleFunc = func() {}
|
||||
|
|
@ -58,7 +59,7 @@ func ImportLastFMFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrainzCall
|
|||
export := make([]LastFMExportPage, 0)
|
||||
err = json.NewDecoder(file).Decode(&export)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ImportLastFMFile: %w", err)
|
||||
}
|
||||
count := 0
|
||||
for _, item := range export {
|
||||
|
|
@ -88,7 +89,8 @@ func ImportLastFMFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrainzCall
|
|||
if err != nil {
|
||||
ts, err = time.Parse("02 Jan 2006, 15:04", track.Date.Text)
|
||||
if err != nil {
|
||||
ts = time.Now().UTC()
|
||||
l.Err(err).Msg("Could not parse time from listen activity, skipping...")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
ts = time.Unix(unix, 0).UTC()
|
||||
|
|
@ -116,11 +118,12 @@ func ImportLastFMFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrainzCall
|
|||
Client: "lastfm",
|
||||
Time: ts,
|
||||
UserID: 1,
|
||||
SkipCacheImage: !cfg.FetchImagesDuringImport(),
|
||||
}
|
||||
err = catalog.SubmitListen(ctx, store, opts)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to import LastFM playback item")
|
||||
return err
|
||||
return fmt.Errorf("ImportLastFMFile: %w", err)
|
||||
}
|
||||
count++
|
||||
throttleFunc()
|
||||
|
|
|
|||
|
|
@ -141,11 +141,12 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai
|
|||
Time: ts,
|
||||
UserID: 1,
|
||||
Client: client,
|
||||
SkipCacheImage: !cfg.FetchImagesDuringImport(),
|
||||
}
|
||||
err = catalog.SubmitListen(ctx, store, opts)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to import LastFM playback item")
|
||||
return err
|
||||
return fmt.Errorf("ImportListenBrainzFile: %w", err)
|
||||
}
|
||||
count++
|
||||
throttleFunc()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package importer
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
|
@ -37,7 +38,7 @@ func ImportMalojaFile(ctx context.Context, store db.DB, filename string) error {
|
|||
file, err := os.Open(path.Join(cfg.ConfigDir(), "import", filename))
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Failed to read import file: %s", filename)
|
||||
return err
|
||||
return fmt.Errorf("ImportMalojaFile: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
var throttleFunc = func() {}
|
||||
|
|
@ -49,7 +50,7 @@ func ImportMalojaFile(ctx context.Context, store db.DB, filename string) error {
|
|||
export := new(MalojaExport)
|
||||
err = json.NewDecoder(file).Decode(&export)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ImportMalojaFile: %w", err)
|
||||
}
|
||||
for _, item := range export.Scrobbles {
|
||||
martists := make([]string, 0)
|
||||
|
|
@ -71,19 +72,20 @@ func ImportMalojaFile(ctx context.Context, store db.DB, filename string) error {
|
|||
continue
|
||||
}
|
||||
opts := catalog.SubmitListenOpts{
|
||||
MbzCaller: &mbz.MusicBrainzClient{},
|
||||
Artist: item.Track.Artists[0],
|
||||
ArtistNames: artists,
|
||||
TrackTitle: item.Track.Title,
|
||||
ReleaseTitle: item.Track.Album.Title,
|
||||
Time: ts.Local(),
|
||||
Client: "maloja",
|
||||
UserID: 1,
|
||||
MbzCaller: &mbz.MusicBrainzClient{},
|
||||
Artist: item.Track.Artists[0],
|
||||
ArtistNames: artists,
|
||||
TrackTitle: item.Track.Title,
|
||||
ReleaseTitle: item.Track.Album.Title,
|
||||
Time: ts.Local(),
|
||||
Client: "maloja",
|
||||
UserID: 1,
|
||||
SkipCacheImage: !cfg.FetchImagesDuringImport(),
|
||||
}
|
||||
err = catalog.SubmitListen(ctx, store, opts)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to import maloja playback item")
|
||||
return err
|
||||
return fmt.Errorf("ImportMalojaFile: %w", err)
|
||||
}
|
||||
throttleFunc()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package importer
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
|
@ -29,7 +30,7 @@ func ImportSpotifyFile(ctx context.Context, store db.DB, filename string) error
|
|||
file, err := os.Open(path.Join(cfg.ConfigDir(), "import", filename))
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("Failed to read import file: %s", filename)
|
||||
return err
|
||||
return fmt.Errorf("ImportSpotifyFile: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
var throttleFunc = func() {}
|
||||
|
|
@ -41,7 +42,7 @@ func ImportSpotifyFile(ctx context.Context, store db.DB, filename string) error
|
|||
export := make([]SpotifyExportItem, 0)
|
||||
err = json.NewDecoder(file).Decode(&export)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ImportSpotifyFile: %w", err)
|
||||
}
|
||||
|
||||
for _, item := range export {
|
||||
|
|
@ -58,19 +59,20 @@ func ImportSpotifyFile(ctx context.Context, store db.DB, filename string) error
|
|||
continue
|
||||
}
|
||||
opts := catalog.SubmitListenOpts{
|
||||
MbzCaller: &mbz.MusicBrainzClient{},
|
||||
Artist: item.ArtistName,
|
||||
TrackTitle: item.TrackName,
|
||||
ReleaseTitle: item.AlbumName,
|
||||
Duration: dur / 1000,
|
||||
Time: item.Timestamp,
|
||||
Client: "spotify",
|
||||
UserID: 1,
|
||||
MbzCaller: &mbz.MusicBrainzClient{},
|
||||
Artist: item.ArtistName,
|
||||
TrackTitle: item.TrackName,
|
||||
ReleaseTitle: item.AlbumName,
|
||||
Duration: dur / 1000,
|
||||
Time: item.Timestamp,
|
||||
Client: "spotify",
|
||||
UserID: 1,
|
||||
SkipCacheImage: !cfg.FetchImagesDuringImport(),
|
||||
}
|
||||
err = catalog.SubmitListen(ctx, store, opts)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to import spotify playback item")
|
||||
return err
|
||||
return fmt.Errorf("ImportSpotifyFile: %w", err)
|
||||
}
|
||||
throttleFunc()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package mbz
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
|
|
@ -28,7 +29,7 @@ func (c *MusicBrainzClient) getArtist(ctx context.Context, id uuid.UUID) (*Music
|
|||
mbzArtist := new(MusicBrainzArtist)
|
||||
err := c.getEntity(ctx, artistAliasFmtStr, id, mbzArtist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("getArtist: %w", err)
|
||||
}
|
||||
return mbzArtist, nil
|
||||
}
|
||||
|
|
@ -38,10 +39,10 @@ func (c *MusicBrainzClient) GetArtistPrimaryAliases(ctx context.Context, id uuid
|
|||
l := logger.FromContext(ctx)
|
||||
artist, err := c.getArtist(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetArtistPrimaryAliases: %w", err)
|
||||
}
|
||||
if artist == nil {
|
||||
return nil, errors.New("artist could not be found by musicbrainz")
|
||||
return nil, errors.New("GetArtistPrimaryAliases: artist could not be found by musicbrainz")
|
||||
}
|
||||
used := make(map[string]bool)
|
||||
ret := make([]string, 1)
|
||||
|
|
|
|||
|
|
@ -52,19 +52,19 @@ func (c *MusicBrainzClient) getEntity(ctx context.Context, fmtStr string, id uui
|
|||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("Failed to build MusicBrainz request")
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
l.Debug().Msg("Adding MusicBrainz request to queue")
|
||||
body, err := c.queue(ctx, req)
|
||||
if err != nil {
|
||||
l.Err(err).Msg("MusicBrainz request failed")
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
l.Err(err).Str("body", string(body)).Msg("Failed to unmarshal MusicBrainz response body")
|
||||
return err
|
||||
return fmt.Errorf("getEntity: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package mbz
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -36,7 +37,7 @@ func (c *MusicBrainzClient) GetReleaseGroup(ctx context.Context, id uuid.UUID) (
|
|||
mbzRG := new(MusicBrainzReleaseGroup)
|
||||
err := c.getEntity(ctx, releaseGroupFmtStr, id, mbzRG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetReleaseGroup: %w", err)
|
||||
}
|
||||
return mbzRG, nil
|
||||
}
|
||||
|
|
@ -45,7 +46,7 @@ func (c *MusicBrainzClient) GetRelease(ctx context.Context, id uuid.UUID) (*Musi
|
|||
mbzRelease := new(MusicBrainzRelease)
|
||||
err := c.getEntity(ctx, releaseFmtStr, id, mbzRelease)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetRelease: %w", err)
|
||||
}
|
||||
return mbzRelease, nil
|
||||
}
|
||||
|
|
@ -53,7 +54,7 @@ func (c *MusicBrainzClient) GetRelease(ctx context.Context, id uuid.UUID) (*Musi
|
|||
func (c *MusicBrainzClient) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
||||
releaseGroup, err := c.GetReleaseGroup(ctx, RGID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetReleaseTitles: %w", err)
|
||||
}
|
||||
|
||||
var titles []string
|
||||
|
|
@ -80,7 +81,7 @@ func ReleaseGroupToTitles(rg *MusicBrainzReleaseGroup) []string {
|
|||
func (c *MusicBrainzClient) GetLatinTitles(ctx context.Context, id uuid.UUID) ([]string, error) {
|
||||
rg, err := c.GetReleaseGroup(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetLatinTitles: %w", err)
|
||||
}
|
||||
titles := make([]string, 0)
|
||||
for _, r := range rg.Releases {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package mbz
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
|
@ -17,7 +18,7 @@ func (c *MusicBrainzClient) GetTrack(ctx context.Context, id uuid.UUID) (*MusicB
|
|||
track := new(MusicBrainzTrack)
|
||||
err := c.getEntity(ctx, recordingFmtStr, id, track)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("GetTrack: %w", err)
|
||||
}
|
||||
return track, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type Artist struct {
|
|||
Image *uuid.UUID `json:"image"`
|
||||
ListenCount int64 `json:"listen_count"`
|
||||
TimeListened int64 `json:"time_listened"`
|
||||
IsPrimary bool `json:"is_primary,omitempty"`
|
||||
}
|
||||
|
||||
type SimpleArtist struct {
|
||||
|
|
|
|||
|
|
@ -199,28 +199,39 @@ func (q *Queries) GetArtistByName(ctx context.Context, alias string) (GetArtistB
|
|||
|
||||
const getReleaseArtists = `-- name: GetReleaseArtists :many
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
ar.is_primary as is_primary
|
||||
FROM artists_with_name a
|
||||
LEFT JOIN artist_releases ar ON a.id = ar.artist_id
|
||||
WHERE ar.release_id = $1
|
||||
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name
|
||||
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, ar.is_primary
|
||||
`
|
||||
|
||||
func (q *Queries) GetReleaseArtists(ctx context.Context, releaseID int32) ([]ArtistsWithName, error) {
|
||||
type GetReleaseArtistsRow struct {
|
||||
ID int32
|
||||
MusicBrainzID *uuid.UUID
|
||||
Image *uuid.UUID
|
||||
ImageSource pgtype.Text
|
||||
Name string
|
||||
IsPrimary pgtype.Bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetReleaseArtists(ctx context.Context, releaseID int32) ([]GetReleaseArtistsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getReleaseArtists, releaseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ArtistsWithName
|
||||
var items []GetReleaseArtistsRow
|
||||
for rows.Next() {
|
||||
var i ArtistsWithName
|
||||
var i GetReleaseArtistsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.MusicBrainzID,
|
||||
&i.Image,
|
||||
&i.ImageSource,
|
||||
&i.Name,
|
||||
&i.IsPrimary,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -297,28 +308,39 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP
|
|||
|
||||
const getTrackArtists = `-- name: GetTrackArtists :many
|
||||
SELECT
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name
|
||||
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
|
||||
at.is_primary as is_primary
|
||||
FROM artists_with_name a
|
||||
LEFT JOIN artist_tracks at ON a.id = at.artist_id
|
||||
WHERE at.track_id = $1
|
||||
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name
|
||||
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, at.is_primary
|
||||
`
|
||||
|
||||
func (q *Queries) GetTrackArtists(ctx context.Context, trackID int32) ([]ArtistsWithName, error) {
|
||||
type GetTrackArtistsRow struct {
|
||||
ID int32
|
||||
MusicBrainzID *uuid.UUID
|
||||
Image *uuid.UUID
|
||||
ImageSource pgtype.Text
|
||||
Name string
|
||||
IsPrimary pgtype.Bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetTrackArtists(ctx context.Context, trackID int32) ([]GetTrackArtistsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTrackArtists, trackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ArtistsWithName
|
||||
var items []GetTrackArtistsRow
|
||||
for rows.Next() {
|
||||
var i ArtistsWithName
|
||||
var i GetTrackArtistsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.MusicBrainzID,
|
||||
&i.Image,
|
||||
&i.ImageSource,
|
||||
&i.Name,
|
||||
&i.IsPrimary,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,12 +194,7 @@ SELECT
|
|||
l.track_id, l.listened_at, l.client, l.user_id,
|
||||
t.title AS track_title,
|
||||
t.release_id AS release_id,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
JOIN artist_tracks at ON t.id = at.track_id
|
||||
|
|
@ -266,12 +261,7 @@ SELECT
|
|||
l.track_id, l.listened_at, l.client, l.user_id,
|
||||
t.title AS track_title,
|
||||
t.release_id AS release_id,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
WHERE l.listened_at BETWEEN $1 AND $2
|
||||
|
|
@ -337,12 +327,7 @@ SELECT
|
|||
l.track_id, l.listened_at, l.client, l.user_id,
|
||||
t.title AS track_title,
|
||||
t.release_id AS release_id,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
WHERE l.listened_at BETWEEN $1 AND $2
|
||||
|
|
@ -408,12 +393,7 @@ SELECT
|
|||
l.track_id, l.listened_at, l.client, l.user_id,
|
||||
t.title AS track_title,
|
||||
t.release_id AS release_id,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
WHERE l.listened_at BETWEEN $1 AND $2
|
||||
|
|
|
|||
|
|
@ -80,11 +80,13 @@ type ArtistAlias struct {
|
|||
type ArtistRelease struct {
|
||||
ArtistID int32
|
||||
ReleaseID int32
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
type ArtistTrack struct {
|
||||
ArtistID int32
|
||||
TrackID int32
|
||||
ArtistID int32
|
||||
TrackID int32
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
type ArtistsWithName struct {
|
||||
|
|
|
|||
|
|
@ -197,12 +197,7 @@ func (q *Queries) GetReleaseByMbzID(ctx context.Context, musicbrainzID *uuid.UUI
|
|||
const getReleasesWithoutImages = `-- name: GetReleasesWithoutImages :many
|
||||
SELECT
|
||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||
(
|
||||
SELECT json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name))
|
||||
FROM artists_with_name a
|
||||
JOIN artist_releases ar ON a.id = ar.artist_id
|
||||
WHERE ar.release_id = r.id
|
||||
) AS artists
|
||||
get_artists_for_release(r.id) AS artists
|
||||
FROM releases_with_title r
|
||||
WHERE r.image IS NULL
|
||||
AND r.id > $2
|
||||
|
|
@ -257,12 +252,7 @@ const getTopReleasesFromArtist = `-- name: GetTopReleasesFromArtist :many
|
|||
SELECT
|
||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||
COUNT(*) AS listen_count,
|
||||
(
|
||||
SELECT json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name))
|
||||
FROM artists_with_name a
|
||||
JOIN artist_releases ar ON ar.artist_id = a.id
|
||||
WHERE ar.release_id = r.id
|
||||
) AS artists
|
||||
get_artists_for_release(r.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks t ON l.track_id = t.id
|
||||
JOIN releases_with_title r ON t.release_id = r.id
|
||||
|
|
@ -332,12 +322,7 @@ const getTopReleasesPaginated = `-- name: GetTopReleasesPaginated :many
|
|||
SELECT
|
||||
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
|
||||
COUNT(*) AS listen_count,
|
||||
(
|
||||
SELECT json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name))
|
||||
FROM artists_with_name a
|
||||
JOIN artist_releases ar ON ar.artist_id = a.id
|
||||
WHERE ar.release_id = r.id
|
||||
) AS artists
|
||||
get_artists_for_release(r.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks t ON l.track_id = t.id
|
||||
JOIN releases_with_title r ON t.release_id = r.id
|
||||
|
|
@ -461,6 +446,22 @@ func (q *Queries) UpdateReleaseMbzID(ctx context.Context, arg UpdateReleaseMbzID
|
|||
return err
|
||||
}
|
||||
|
||||
const updateReleasePrimaryArtist = `-- name: UpdateReleasePrimaryArtist :exec
|
||||
UPDATE artist_releases SET is_primary = $3
|
||||
WHERE artist_id = $1 AND release_id = $2
|
||||
`
|
||||
|
||||
type UpdateReleasePrimaryArtistParams struct {
|
||||
ArtistID int32
|
||||
ReleaseID int32
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateReleasePrimaryArtist(ctx context.Context, arg UpdateReleasePrimaryArtistParams) error {
|
||||
_, err := q.db.Exec(ctx, updateReleasePrimaryArtist, arg.ArtistID, arg.ReleaseID, arg.IsPrimary)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateReleaseVariousArtists = `-- name: UpdateReleaseVariousArtists :exec
|
||||
UPDATE releases SET various_artists = $2
|
||||
WHERE id = $1
|
||||
|
|
|
|||
|
|
@ -136,12 +136,7 @@ SELECT
|
|||
ranked.image,
|
||||
ranked.various_artists,
|
||||
ranked.score,
|
||||
(
|
||||
SELECT json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name))
|
||||
FROM artists_with_name a
|
||||
JOIN artist_releases ar ON ar.artist_id = a.id
|
||||
WHERE ar.release_id = ranked.id
|
||||
) AS artists
|
||||
get_artists_for_release(ranked.id) AS artists
|
||||
FROM (
|
||||
SELECT
|
||||
r.id,
|
||||
|
|
@ -211,12 +206,7 @@ SELECT
|
|||
ranked.image,
|
||||
ranked.various_artists,
|
||||
ranked.score,
|
||||
(
|
||||
SELECT json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name))
|
||||
FROM artists_with_name a
|
||||
JOIN artist_releases ar ON ar.artist_id = a.id
|
||||
WHERE ar.release_id = ranked.id
|
||||
) AS artists
|
||||
get_artists_for_release(ranked.id) AS artists
|
||||
FROM (
|
||||
SELECT
|
||||
r.id,
|
||||
|
|
@ -286,12 +276,7 @@ SELECT
|
|||
ranked.release_id,
|
||||
ranked.image,
|
||||
ranked.score,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = ranked.id
|
||||
) AS artists
|
||||
get_artists_for_track(ranked.id) AS artists
|
||||
FROM (
|
||||
SELECT
|
||||
t.id,
|
||||
|
|
@ -362,12 +347,7 @@ SELECT
|
|||
ranked.release_id,
|
||||
ranked.image,
|
||||
ranked.score,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = ranked.id
|
||||
) AS artists
|
||||
get_artists_for_track(ranked.id) AS artists
|
||||
FROM (
|
||||
SELECT
|
||||
t.id,
|
||||
|
|
|
|||
|
|
@ -138,12 +138,7 @@ SELECT
|
|||
t.release_id,
|
||||
r.image,
|
||||
COUNT(*) AS listen_count,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at2
|
||||
JOIN artists_with_name a ON a.id = at2.artist_id
|
||||
WHERE at2.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
JOIN releases r ON t.release_id = r.id
|
||||
|
|
@ -215,12 +210,7 @@ SELECT
|
|||
t.release_id,
|
||||
r.image,
|
||||
COUNT(*) AS listen_count,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at2
|
||||
JOIN artists_with_name a ON a.id = at2.artist_id
|
||||
WHERE at2.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
JOIN releases r ON t.release_id = r.id
|
||||
|
|
@ -291,12 +281,7 @@ SELECT
|
|||
t.release_id,
|
||||
r.image,
|
||||
COUNT(*) AS listen_count,
|
||||
(
|
||||
SELECT json_agg(json_build_object('id', a.id, 'name', a.name))
|
||||
FROM artist_tracks at
|
||||
JOIN artists_with_name a ON a.id = at.artist_id
|
||||
WHERE at.track_id = t.id
|
||||
) AS artists
|
||||
get_artists_for_track(t.id) AS artists
|
||||
FROM listens l
|
||||
JOIN tracks_with_title t ON l.track_id = t.id
|
||||
JOIN releases r ON t.release_id = r.id
|
||||
|
|
@ -502,3 +487,19 @@ func (q *Queries) UpdateTrackMbzID(ctx context.Context, arg UpdateTrackMbzIDPara
|
|||
_, err := q.db.Exec(ctx, updateTrackMbzID, arg.ID, arg.MusicBrainzID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTrackPrimaryArtist = `-- name: UpdateTrackPrimaryArtist :exec
|
||||
UPDATE artist_tracks SET is_primary = $3
|
||||
WHERE artist_id = $1 AND track_id = $2
|
||||
`
|
||||
|
||||
type UpdateTrackPrimaryArtistParams struct {
|
||||
ArtistID int32
|
||||
TrackID int32
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateTrackPrimaryArtist(ctx context.Context, arg UpdateTrackPrimaryArtistParams) error {
|
||||
_, err := q.db.Exec(ctx, updateTrackPrimaryArtist, arg.ArtistID, arg.TrackID, arg.IsPrimary)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,22 +90,22 @@ func DateRange(week, month, year int) (time.Time, time.Time, error) {
|
|||
}
|
||||
|
||||
if month != 0 && (month < 1 || month > 12) {
|
||||
return time.Time{}, time.Time{}, errors.New("invalid month")
|
||||
return time.Time{}, time.Time{}, errors.New("DateRange: invalid month")
|
||||
}
|
||||
|
||||
if week != 0 && (week < 1 || week > 53) {
|
||||
return time.Time{}, time.Time{}, errors.New("invalid week")
|
||||
return time.Time{}, time.Time{}, errors.New("DateRange: invalid week")
|
||||
}
|
||||
|
||||
if year < 1 {
|
||||
return time.Time{}, time.Time{}, errors.New("invalid year")
|
||||
return time.Time{}, time.Time{}, errors.New("DateRange: invalid year")
|
||||
}
|
||||
|
||||
loc := time.Local
|
||||
|
||||
if week != 0 {
|
||||
if month != 0 {
|
||||
return time.Time{}, time.Time{}, errors.New("cannot specify both week and month")
|
||||
return time.Time{}, time.Time{}, errors.New("DateRange: cannot specify both week and month")
|
||||
}
|
||||
// Specific week
|
||||
start := time.Date(year, 1, 1, 0, 0, 0, 0, loc)
|
||||
|
|
@ -133,31 +133,34 @@ func DateRange(week, month, year int) (time.Time, time.Time, error) {
|
|||
func CopyFile(src, dst string) (err error) {
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
return fmt.Errorf("CopyFile: %w", err)
|
||||
}
|
||||
if !sfi.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories,
|
||||
// symlinks, devices, etc.)
|
||||
return fmt.Errorf("non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||
}
|
||||
dfi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
return fmt.Errorf("CopyFile: %w", err)
|
||||
}
|
||||
} else {
|
||||
if !(dfi.Mode().IsRegular()) {
|
||||
return fmt.Errorf("non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
}
|
||||
if os.SameFile(sfi, dfi) {
|
||||
return
|
||||
return fmt.Errorf("CopyFile: %w", err)
|
||||
}
|
||||
}
|
||||
if err = os.Link(src, dst); err == nil {
|
||||
return
|
||||
return fmt.Errorf("CopyFile: %w", err)
|
||||
}
|
||||
err = copyFileContents(src, dst)
|
||||
return
|
||||
if err != nil {
|
||||
return fmt.Errorf("CopyFile: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFileContents copies the contents of the file named src to the file named
|
||||
|
|
@ -167,24 +170,22 @@ func CopyFile(src, dst string) (err error) {
|
|||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
return fmt.Errorf("copyFileContents: %w", err)
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
return fmt.Errorf("copyFileContents: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
defer out.Close()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
return fmt.Errorf("copyFileContents: %w", err)
|
||||
}
|
||||
err = out.Sync()
|
||||
return
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFileContents: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the same slice, but with all strings that are equal (with strings.EqualFold)
|
||||
|
|
@ -281,7 +282,7 @@ func GenerateRandomString(length int) (string, error) {
|
|||
for i := range length {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("GenerateRandomString: %w", err)
|
||||
}
|
||||
ret[i] = letters[num.Int64()]
|
||||
}
|
||||
|
|
@ -311,3 +312,18 @@ func MoreThanOneString(s ...string) bool {
|
|||
}
|
||||
return count > 1
|
||||
}
|
||||
|
||||
func ParseBool(s string) (value, ok bool) {
|
||||
if strings.ToLower(s) == "true" {
|
||||
value = true
|
||||
ok = true
|
||||
return
|
||||
} else if strings.ToLower(s) == "false" {
|
||||
value = false
|
||||
ok = true
|
||||
return
|
||||
} else {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue