release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped
- acquisition_settings.upgrade_until (default High quality lossy, Lidarr's cutoff, note #5527; "" is off), an "Upgrade until" row under Searching. - The wanted worker judges held albums hourly: tier per format from size per second (the scanner records no bitrate), the album's worst. Below the cutoff it is wanted for quality; a video rip among its tracks always. - The schedule searches upgrades weekly, after open wants; a release no better than the copy held is refused ("not_better"). - The import matches each file to the track it would replace (recording, then disc/track) and replaces it when it is a rip or the file is a better tier: over the same path in one rename, or filed beside it and the old track folded in (library.ReplaceTrack: file first, then foldTrackInto). Nothing better: the release is set aside. - Collection › Upgrades (Lidarr's Cutoff Unmet); Suspect sources says when a clean copy was looked for instead of Lidarr's search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.6 KiB
Go
84 lines
2.6 KiB
Go
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
|
|
}
|