package psql import ( "context" "errors" "strings" "time" "github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/models" "github.com/gabehf/koito/internal/repository" "github.com/gabehf/koito/internal/utils" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Album, error) { l := logger.FromContext(ctx) var row repository.ReleasesWithTitle var err error if opts.ID != 0 { l.Debug().Msgf("Fetching album from DB with id %d", opts.ID) row, err = d.q.GetRelease(ctx, opts.ID) } else if opts.MusicBrainzID != uuid.Nil { l.Debug().Msgf("Fetching album from DB with MusicBrainz Release ID %s", opts.MusicBrainzID) row, err = d.q.GetReleaseByMbzID(ctx, &opts.MusicBrainzID) } else if opts.ArtistID != 0 && opts.Title != "" { l.Debug().Msgf("Fetching album from DB with artist_id %d and title %s", opts.ArtistID, opts.Title) row, err = d.q.GetReleaseByArtistAndTitle(ctx, repository.GetReleaseByArtistAndTitleParams{ ArtistID: opts.ArtistID, Title: opts.Title, }) } else if opts.ArtistID != 0 && len(opts.Titles) > 0 { l.Debug().Msgf("Fetching release group from DB with artist_id %d and titles %v", opts.ArtistID, opts.Titles) row, err = d.q.GetReleaseByArtistAndTitles(ctx, repository.GetReleaseByArtistAndTitlesParams{ ArtistID: opts.ArtistID, Column1: opts.Titles, }) } else { return nil, errors.New("insufficient information to get album") } if err != nil { return nil, err } count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{ ListenedAt: time.Unix(0, 0), ListenedAt_2: time.Now(), ReleaseID: row.ID, }) if err != nil { return nil, err } seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ Period: db.PeriodAllTime, AlbumID: row.ID, }) if err != nil { return nil, err } return &models.Album{ ID: row.ID, MbzID: row.MusicBrainzID, Title: row.Title, Image: row.Image, VariousArtists: row.VariousArtists, ListenCount: count, TimeListened: seconds, }, nil } func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Album, error) { l := logger.FromContext(ctx) var insertMbzID *uuid.UUID var insertImage *uuid.UUID if opts.MusicBrainzID != uuid.Nil { insertMbzID = &opts.MusicBrainzID } if opts.Image != uuid.Nil { insertImage = &opts.Image } if len(opts.ArtistIDs) < 1 { return nil, errors.New("required parameter 'ArtistIDs' missing") } for _, aid := range opts.ArtistIDs { if aid == 0 { return nil, errors.New("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 } defer tx.Rollback(ctx) qtx := d.q.WithTx(tx) l.Debug().Msgf("Inserting release '%s' into DB", opts.Title) r, err := qtx.InsertRelease(ctx, repository.InsertReleaseParams{ MusicBrainzID: insertMbzID, VariousArtists: opts.VariousArtists, Image: insertImage, ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""}, }) if err != nil { return nil, err } for _, artistId := range opts.ArtistIDs { l.Debug().Msgf("Associating release '%s' to artist with ID %d", opts.Title, artistId) err = qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{ ArtistID: artistId, ReleaseID: r.ID, }) if err != nil { return nil, err } } l.Debug().Msgf("Saving canonical alias %s for release %d", opts.Title, r.ID) err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{ ReleaseID: r.ID, Alias: opts.Title, Source: "Canonical", IsPrimary: true, }) if err != nil { l.Err(err).Msgf("Failed to save canonical alias for album %d", r.ID) } err = tx.Commit(ctx) if err != nil { return nil, err } return &models.Album{ ID: r.ID, MbzID: r.MusicBrainzID, Title: opts.Title, Image: r.Image, VariousArtists: r.VariousArtists, }, nil } func (d *Psql) AddArtistsToAlbum(ctx context.Context, opts db.AddArtistsToAlbumOpts) error { l := logger.FromContext(ctx) tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{}) if err != nil { l.Err(err).Msg("Failed to begin transaction") return err } defer tx.Rollback(ctx) qtx := d.q.WithTx(tx) for _, id := range opts.ArtistIDs { err := qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{ ReleaseID: opts.AlbumID, ArtistID: id, }) if err != nil { l.Error().Err(err).Msgf("Failed to associate release %d with artist %d", opts.AlbumID, id) } } return tx.Commit(ctx) } func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error { l := logger.FromContext(ctx) if opts.ID == 0 { return errors.New("missing album id") } tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{}) if err != nil { l.Err(err).Msg("Failed to begin transaction") return err } defer tx.Rollback(ctx) qtx := d.q.WithTx(tx) if opts.MusicBrainzID != uuid.Nil { l.Debug().Msgf("Updating release with ID %d with MusicBrainz ID %s", opts.ID, opts.MusicBrainzID) err := qtx.UpdateReleaseMbzID(ctx, repository.UpdateReleaseMbzIDParams{ ID: opts.ID, MusicBrainzID: &opts.MusicBrainzID, }) if err != nil { return err } } if opts.Image != uuid.Nil { l.Debug().Msgf("Updating release with ID %d with image %s", opts.ID, opts.Image) err := qtx.UpdateReleaseImage(ctx, repository.UpdateReleaseImageParams{ ID: opts.ID, Image: &opts.Image, ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""}, }) if err != nil { return err } } return tx.Commit(ctx) } func (d *Psql) SaveAlbumAliases(ctx context.Context, id int32, aliases []string, source string) error { l := logger.FromContext(ctx) if id == 0 { return errors.New("album id not specified") } tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{}) if err != nil { l.Err(err).Msg("Failed to begin transaction") return err } defer tx.Rollback(ctx) qtx := d.q.WithTx(tx) existing, err := qtx.GetAllReleaseAliases(ctx, id) if err != nil { return 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") } err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{ Alias: strings.TrimSpace(alias), ReleaseID: id, Source: source, IsPrimary: false, }) if err != nil { return err } } return tx.Commit(ctx) } func (d *Psql) DeleteAlbum(ctx context.Context, id int32) error { return d.q.DeleteRelease(ctx, id) } func (d *Psql) DeleteAlbumAlias(ctx context.Context, id int32, alias string) error { return d.q.DeleteReleaseAlias(ctx, repository.DeleteReleaseAliasParams{ ReleaseID: id, Alias: alias, }) } 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 } aliases := make([]models.Alias, len(rows)) for i, row := range rows { aliases[i] = models.Alias{ ID: id, Alias: row.Alias, Source: row.Source, Primary: row.IsPrimary, } } return aliases, nil } func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string) 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 err } defer tx.Rollback(ctx) qtx := d.q.WithTx(tx) // get all aliases aliases, err := qtx.GetAllReleaseAliases(ctx, id) if err != nil { return err } primary := "" exists := false for _, v := range aliases { if v.Alias == alias { exists = true } if v.IsPrimary { primary = v.Alias } } if primary == alias { // no-op rename return nil } if !exists { return errors.New("alias does not exist") } err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{ ReleaseID: id, Alias: alias, IsPrimary: true, }) if err != nil { return err } err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{ ReleaseID: id, Alias: primary, IsPrimary: false, }) if err != nil { return err } return tx.Commit(ctx) }