feat: a held album below the quality you settle for, or holding a video rip, stays wanted for a better copy, and the import replaces the old tracks with their likes and plays carried over (M502 #5524)
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
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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user