package library import ( "context" "errors" "fmt" "log/slog" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/coverart" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync" ) // Replacing a track with a better copy (M502 #5524). // // An upgrade files the better copy first, as a track of its own, and only // then replaces the old one: its file is removed, then its likes, plays, // playlist entries and tags move onto the new row and the old row goes. It // is the duplicate merge with the survivor chosen by the import, and it // keeps the merge's order (#3918): if the old file cannot be removed nothing // in the database changes, and the next scan finds the two copies as a // duplicate pair. // ErrReplaceSame: the old and new track are one row. var ErrReplaceSame = errors.New("library: a track cannot replace itself") // Replaced is what one replacement moved onto the new track. type Replaced struct { PlayEvents int64 SkipEvents int64 Likes int PlaylistEntries int } // ReplaceTrack replaces oldID with newID: the old file is removed, then the // old row folded into the new one. dataDir, when set, also clears the cached // art of an artist the fold left with nothing. logger may be nil. func ReplaceTrack( ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, oldID, newID pgtype.UUID, ) (Replaced, error) { if logger == nil { logger = slog.Default() } if oldID == newID { return Replaced{}, ErrReplaceSame } old, err := dbq.New(pool).GetTrackByID(ctx, oldID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Replaced{}, ErrTrackNotFound } return Replaced{}, fmt.Errorf("get track: %w", err) } if err := removeTrackFileOnDisk(old.FilePath); err != nil { return Replaced{}, err } var f foldResult err = pgx.BeginFunc(ctx, pool, func(tx pgx.Tx) error { changes := mergeChanges{} var ferr error f, ferr = foldTrackInto(ctx, dbq.New(tx), newID, oldID, &changes) if ferr != nil { return ferr } return changes.log(ctx, tx) }) if err != nil { return Replaced{}, fmt.Errorf("fold the old track into the new: %w", err) } if f.DeletedArtistID != nil && dataDir != "" { if err := coverart.CleanupArtistArt(dataDir, *f.DeletedArtistID); err != nil { logger.Warn("track replace: artist-art cleanup failed", "artist_id", syncpkg.FormatUUID(*f.DeletedArtistID), "err", err) } } return Replaced{PlayEvents: f.PlayEvents, SkipEvents: f.SkipEvents, Likes: len(f.Likers), PlaylistEntries: f.PlaylistEntries}, nil }