mirror of https://github.com/gabehf/Koito.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
13 KiB
428 lines
13 KiB
package psql
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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 err error
|
|
var ret = new(models.Album)
|
|
|
|
if opts.ID != 0 {
|
|
l.Debug().Msgf("Fetching album from DB with id %d", opts.ID)
|
|
row, err := d.q.GetRelease(ctx, opts.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: %w", err)
|
|
}
|
|
ret.ID = row.ID
|
|
ret.MbzID = row.MusicBrainzID
|
|
ret.Title = row.Title
|
|
ret.Image = row.Image
|
|
ret.VariousArtists = row.VariousArtists
|
|
err = json.Unmarshal(row.Artists, &ret.Artists)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: json.Unmarshal: %w", err)
|
|
}
|
|
} 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)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: %w", err)
|
|
}
|
|
ret.ID = row.ID
|
|
ret.MbzID = row.MusicBrainzID
|
|
ret.Title = row.Title
|
|
ret.Image = row.Image
|
|
ret.VariousArtists = row.VariousArtists
|
|
} 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,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: %w", err)
|
|
}
|
|
ret.ID = row.ID
|
|
ret.MbzID = row.MusicBrainzID
|
|
ret.Title = row.Title
|
|
ret.Image = row.Image
|
|
ret.VariousArtists = row.VariousArtists
|
|
} 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,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: %w", err)
|
|
}
|
|
ret.ID = row.ID
|
|
ret.MbzID = row.MusicBrainzID
|
|
ret.Title = row.Title
|
|
ret.Image = row.Image
|
|
ret.VariousArtists = row.VariousArtists
|
|
} else {
|
|
return nil, errors.New("GetAlbum: insufficient information to get album")
|
|
}
|
|
|
|
count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
|
|
ListenedAt: time.Unix(0, 0),
|
|
ListenedAt_2: time.Now(),
|
|
ReleaseID: ret.ID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err)
|
|
}
|
|
|
|
seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
|
|
Period: db.PeriodAllTime,
|
|
AlbumID: ret.ID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
|
|
}
|
|
|
|
firstListen, err := d.q.GetFirstListenFromRelease(ctx, ret.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err)
|
|
}
|
|
|
|
ret.ListenCount = count
|
|
ret.TimeListened = seconds
|
|
ret.FirstListen = firstListen.ListenedAt.Unix()
|
|
|
|
return ret, 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("SaveAlbum: required parameter 'ArtistIDs' missing")
|
|
}
|
|
for _, aid := range opts.ArtistIDs {
|
|
if aid == 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, fmt.Errorf("SaveAlbum: BeginTx: %w", 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, 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)
|
|
err = qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{
|
|
ArtistID: artistId,
|
|
ReleaseID: r.ID,
|
|
IsPrimary: opts.ArtistIDs[0] == artistId,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SaveAlbum: AssociateArtistToRelease: %w", 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)
|
|
return nil, fmt.Errorf("SaveAlbum: InsertReleaseAlias: %w", err)
|
|
}
|
|
|
|
err = tx.Commit(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SaveAlbum: Commit: %w", err)
|
|
}
|
|
|
|
err = d.SaveAlbumAliases(ctx, r.ID, opts.Aliases, "MusicBrainz")
|
|
if err != nil {
|
|
l.Err(err).Msgf("Failed to save aliases for album %s", opts.Title)
|
|
}
|
|
|
|
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 fmt.Errorf("AddArtistsToAlbum: BeginTx: %w", 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 fmt.Errorf("AddArtistsToAlbum: AssociateArtistToRelease: %w", err)
|
|
}
|
|
}
|
|
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 fmt.Errorf("UpdateAlbum: BeginTx: %w", 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 fmt.Errorf("UpdateAlbum: UpdateReleaseMbzID: %w", 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 fmt.Errorf("UpdateAlbum: UpdateReleaseImage: %w", err)
|
|
}
|
|
}
|
|
if opts.VariousArtistsUpdate {
|
|
l.Debug().Msgf("Updating release with ID %d with image %s", opts.ID, opts.Image)
|
|
err := qtx.UpdateReleaseVariousArtists(ctx, repository.UpdateReleaseVariousArtistsParams{
|
|
ID: opts.ID,
|
|
VariousArtists: opts.VariousArtistsValue,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("UpdateAlbum: UpdateReleaseVariousArtists: %w", 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 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 fmt.Errorf("SaveAlbumAliases: GetAllReleaseAliases: %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("SaveAlbumAliases: aliases cannot be blank")
|
|
}
|
|
err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{
|
|
Alias: strings.TrimSpace(alias),
|
|
ReleaseID: id,
|
|
Source: source,
|
|
IsPrimary: false,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("SaveAlbumAliases: InsertReleaseAlias: %w", 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, fmt.Errorf("GetAllAlbumAliases: GetAllReleaseAliases: %w", 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 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 fmt.Errorf("SetPrimaryAlbumAlias: GetAllReleaseAliases: %w", 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("SetPrimaryAlbumAlias: alias does not exist")
|
|
}
|
|
err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
|
|
ReleaseID: id,
|
|
Alias: alias,
|
|
IsPrimary: true,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("SetPrimaryAlbumAlias: SetReleaseAliasPrimaryStatus: %w", err)
|
|
}
|
|
err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
|
|
ReleaseID: id,
|
|
Alias: primary,
|
|
IsPrimary: false,
|
|
})
|
|
if err != nil {
|
|
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)
|
|
}
|