feat: one recording cut to several songs' lengths is named by AcoustID; the copy in its slot stays, the rest go, and the release is set aside (#6169)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 5m1s
release / integration (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 5m1s
release / integration (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
Savant's Mortals held one recording as 16 of its 50 tracks, each trimmed to its own slot's MusicBrainz length, so the lengths could not say which slot was its own and every group waited for the operator. When a one-album mismatch is the same audio (identical, or fingerprints within 0.02) and every copy fits its own slot, the resolver asks what the recording is: the copies' own AcoustID lookups, or a fresh lookup of one copy (five a pass). The track list names each slot's recording; the copy in that slot is kept and the others removed, or every copy when the recording is in none of their slots or AcoustID knows none. AcoustID unreachable or answers in several copies' slots: the group waits. Whenever wrong-slot copies are removed, the download the files were imported from is set aside, so the album fetched again is not the same files. Albums imported before Minstrel downloaded anything have none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,9 @@ import (
|
||||
// - Two versions of a song on one album that sound alike are not duplicates:
|
||||
// the group is dismissed (duplicate_settle.go).
|
||||
// - One file filling several of an album's track slots: MusicBrainz's track
|
||||
// lengths say which slot it belongs to, and the copies in the other slots
|
||||
// are removed so the album is fetched again (duplicate_settle.go).
|
||||
// lengths say which slot it belongs to, or AcoustID which song it is when
|
||||
// each copy was cut to its own slot's length, and the copies in the other
|
||||
// slots are removed so the album is fetched again (duplicate_settle.go).
|
||||
// - Anything else waits for the operator.
|
||||
|
||||
const (
|
||||
@@ -65,11 +66,14 @@ type DuplicateResolveResult struct {
|
||||
|
||||
// resolveGroup is one pending group and its present copies.
|
||||
type resolveGroup struct {
|
||||
id pgtype.UUID
|
||||
tier string
|
||||
members []dbq.ListDuplicateGroupsForResolveRow
|
||||
class DuplicateClass
|
||||
note string
|
||||
id pgtype.UUID
|
||||
tier string
|
||||
// worstBER is how far apart the least alike copies' fingerprints are, for
|
||||
// an acoustic group; nil for an exact one.
|
||||
worstBER *float32
|
||||
members []dbq.ListDuplicateGroupsForResolveRow
|
||||
class DuplicateClass
|
||||
note string
|
||||
}
|
||||
|
||||
// isRip says whether a copy is a video rip: held back as one, or named like
|
||||
@@ -116,20 +120,33 @@ func mergePlan(g *resolveGroup) (ok bool, survivor int) {
|
||||
// MusicBrainz into the catalogue (catalogue.Service.RefreshReleaseGroup).
|
||||
type TrackListFetcher func(ctx context.Context, releaseGroupMBID string) error
|
||||
|
||||
// RecordingIdentifier asks AcoustID which recordings a file may be
|
||||
// (AcoustIDLookupWorker.IdentifyRecordings).
|
||||
type RecordingIdentifier func(ctx context.Context, path string) ([]string, error)
|
||||
|
||||
// ResolveSources are what a pass may ask beyond the library, a few times per
|
||||
// pass each. Either may be nil, and the pass decides from what is held.
|
||||
type ResolveSources struct {
|
||||
// TrackLists reads the track list of an album whose wrong slot the pass
|
||||
// must decide and the catalogue has never read (trackListFetchesPerPass).
|
||||
TrackLists TrackListFetcher
|
||||
// Identify names the recording cut to several slots' lengths, when no
|
||||
// copy's own lookup has (identifiesPerPass).
|
||||
Identify RecordingIdentifier
|
||||
}
|
||||
|
||||
// ResolveDuplicates runs one pass. act is the operator's auto-resolve setting;
|
||||
// with it off, the pass only classifies. It reads only track lists already in
|
||||
// the catalogue; ResolveDuplicatesFetching may also fetch missing ones.
|
||||
// with it off, the pass only classifies. It decides from what the library and
|
||||
// catalogue hold; ResolveDuplicatesWith may also ask MusicBrainz and AcoustID.
|
||||
func ResolveDuplicates(
|
||||
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, act bool,
|
||||
) (DuplicateResolveResult, error) {
|
||||
return ResolveDuplicatesFetching(ctx, pool, logger, dataDir, act, nil)
|
||||
return ResolveDuplicatesWith(ctx, pool, logger, dataDir, act, ResolveSources{})
|
||||
}
|
||||
|
||||
// ResolveDuplicatesFetching is ResolveDuplicates that asks MusicBrainz, through
|
||||
// fetch, for the track list of an album whose wrong slot it must decide and
|
||||
// the catalogue has never read: a few per pass (trackListFetchesPerPass).
|
||||
func ResolveDuplicatesFetching(
|
||||
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, act bool, fetch TrackListFetcher,
|
||||
// ResolveDuplicatesWith is ResolveDuplicates that may ask src.
|
||||
func ResolveDuplicatesWith(
|
||||
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, act bool, src ResolveSources,
|
||||
) (DuplicateResolveResult, error) {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
@@ -196,7 +213,7 @@ func ResolveDuplicatesFetching(
|
||||
}
|
||||
|
||||
res.Dismissed = dismissDistinct(ctx, pool, logger, groups, merged)
|
||||
wrong, names := settleWrongSlots(ctx, pool, logger, groups, merged, fetch)
|
||||
wrong, names := settleWrongSlots(ctx, pool, logger, groups, merged, src)
|
||||
res.WrongFiles = wrong
|
||||
mergedNames = append(mergedNames, names...)
|
||||
|
||||
@@ -281,7 +298,7 @@ func foldResolveGroups(rows []dbq.ListDuplicateGroupsForResolveRow) []*resolveGr
|
||||
var groups []*resolveGroup
|
||||
for _, r := range rows {
|
||||
if n := len(groups); n == 0 || groups[n-1].id != r.GroupID {
|
||||
groups = append(groups, &resolveGroup{id: r.GroupID, tier: r.Tier})
|
||||
groups = append(groups, &resolveGroup{id: r.GroupID, tier: r.Tier, worstBER: r.WorstBitErrorRate})
|
||||
}
|
||||
g := groups[len(groups)-1]
|
||||
g.members = append(g.members, r)
|
||||
@@ -389,17 +406,17 @@ type DuplicateResolveWorker struct {
|
||||
logger *slog.Logger
|
||||
dataDir string
|
||||
settings *FingerprintSettingsService
|
||||
fetch TrackListFetcher
|
||||
src ResolveSources
|
||||
tick time.Duration
|
||||
}
|
||||
|
||||
// NewDuplicateResolveWorker builds a worker with the production cadence. fetch
|
||||
// reads a missing track list from MusicBrainz; nil reads only what is held.
|
||||
// NewDuplicateResolveWorker builds a worker with the production cadence. src
|
||||
// is what a pass may ask beyond the library.
|
||||
func NewDuplicateResolveWorker(
|
||||
pool *pgxpool.Pool, logger *slog.Logger, dataDir string, settings *FingerprintSettingsService, fetch TrackListFetcher,
|
||||
pool *pgxpool.Pool, logger *slog.Logger, dataDir string, settings *FingerprintSettingsService, src ResolveSources,
|
||||
) *DuplicateResolveWorker {
|
||||
return &DuplicateResolveWorker{
|
||||
pool: pool, logger: logger, dataDir: dataDir, settings: settings, fetch: fetch, tick: duplicateResolveTick,
|
||||
pool: pool, logger: logger, dataDir: dataDir, settings: settings, src: src, tick: duplicateResolveTick,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +453,7 @@ func (w *DuplicateResolveWorker) tickOnce(ctx context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
res, err := ResolveDuplicatesFetching(ctx, w.pool, w.logger, w.dataDir, w.settings.Get().AutoResolve, w.fetch)
|
||||
res, err := ResolveDuplicatesWith(ctx, w.pool, w.logger, w.dataDir, w.settings.Get().AutoResolve, w.src)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
w.logger.Warn("duplicate resolve: pass failed", "err", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user