feat: v0.0.8

This commit is contained in:
Gabe Farrell 2025-06-16 21:55:39 -04:00
parent 00e7782be2
commit 80b6f4deaa
66 changed files with 1559 additions and 916 deletions

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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{

View file

@ -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)
}

View file

@ -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 {

View file

@ -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{

View file

@ -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()

View file

@ -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

View file

@ -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{

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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")
}

View file

@ -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
}

View file

@ -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()

View file

@ -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()

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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,

View file

@ -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
}

View file

@ -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
}
}