feat: video rips and stray copies handle themselves (M498 #5439)
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 26s
release / go (push) Failing after 58s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m25s
release / Build signed APK (releases and dev) (push) Canceled after 3m26s
release / android (push) Canceled after 3m28s
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 26s
release / go (push) Failing after 58s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m25s
release / Build signed APK (releases and dev) (push) Canceled after 3m26s
release / android (push) Canceled after 3m28s
- The resolver also merges cross-release and mismatch groups where Lidarr
maps exactly one copy: the others fulfil nothing, so removing them opens no
hole (D-a rule 1). That covers a rip beside the clean copy on another
release and a wrong-file import Lidarr holds unmapped. With two or more
mapped copies each fulfils its own release and nothing is removed.
- A track whose file name carries a video-rip marker is held back from radio
and the system mixes (tracks.source_verdict, migration 0077). It still plays
when chosen. A renamed file is released; the operator's "fine" sticks.
- Suspect sources shows what was done to each track, with "This one is fine"
and "Hold back again" (PUT /api/admin/library/suspect-sources/{id}).
- The Liked list prefers a copy that is not held back.
Replacing a rip that has no clean copy is left for the operator to decide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,9 @@ type DuplicateResolveResult struct {
|
||||
ReleaseChanges []ReleaseChange
|
||||
// SongsLinked counts cross-release groups newly linked as one song.
|
||||
SongsLinked int
|
||||
// HeldBack counts tracks newly held back from radio and the mixes as video
|
||||
// rips; Released counts held-back tracks whose name no longer says so.
|
||||
HeldBack, Released int64
|
||||
}
|
||||
|
||||
// LidarrPathKey is the part of a path Minstrel and Lidarr agree on: the last
|
||||
@@ -115,6 +118,35 @@ type resolveGroup struct {
|
||||
states []string // per member: tracked, unmapped, or "" when Lidarr was not asked
|
||||
}
|
||||
|
||||
// autoMergeable says whether the resolver may merge the group on its own: when
|
||||
// every copy it would remove is one Lidarr does not map (D-a rule 1). On one
|
||||
// album that is any group with at most one mapped copy. Across releases each
|
||||
// mapped copy fulfils its own release (D-b), so only a group with exactly one
|
||||
// mapped copy qualifies: the others fulfil nothing — a rip beside the clean
|
||||
// copy on another release, or a wrong-file import Lidarr holds unmapped
|
||||
// (#5439) — and the mapped copy is the clear one to keep.
|
||||
func autoMergeable(g *resolveGroup) bool {
|
||||
switch g.class {
|
||||
case ClassSameRelease:
|
||||
return g.tracked() <= 1
|
||||
case ClassCrossRelease, ClassMismatch:
|
||||
return g.tracked() == 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// settling says whether any of the group's albums had its Lidarr release
|
||||
// changed recently, so Lidarr may still be remapping its files.
|
||||
func (g *resolveGroup) settling(albums map[string]bool) bool {
|
||||
for _, m := range g.members {
|
||||
if albums[syncpkg.FormatUUID(m.AlbumID)] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *resolveGroup) tracked() int {
|
||||
n := 0
|
||||
for _, s := range g.states {
|
||||
@@ -183,7 +215,14 @@ func ResolveDuplicates(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Linking removes nothing and needs nothing from Lidarr.
|
||||
// Holding rips back and linking songs remove nothing and need nothing from
|
||||
// Lidarr.
|
||||
if res.HeldBack, err = q.FlagSuspectSourceTracks(ctx, SuspectSourcePattern); err != nil {
|
||||
return res, fmt.Errorf("hold back video rips: %w", err)
|
||||
}
|
||||
if res.Released, err = q.ClearStaleSuspectSourceTracks(ctx, SuspectSourcePattern); err != nil {
|
||||
return res, fmt.Errorf("release renamed tracks: %w", err)
|
||||
}
|
||||
linked, err := linkCrossRelease(ctx, pool, logger, groups)
|
||||
res.SongsLinked = linked
|
||||
if err != nil {
|
||||
@@ -208,7 +247,7 @@ func ResolveDuplicates(
|
||||
if ctx.Err() != nil {
|
||||
return res, ctx.Err()
|
||||
}
|
||||
if g.class != ClassSameRelease || g.tracked() > 1 || settling[syncpkg.FormatUUID(g.members[0].AlbumID)] {
|
||||
if !autoMergeable(g) || g.settling(settling) {
|
||||
continue
|
||||
}
|
||||
if err := autoMerge(ctx, pool, logger, dataDir, g, removable); err != nil {
|
||||
@@ -747,5 +786,6 @@ func (w *DuplicateResolveWorker) tickOnce(ctx context.Context) {
|
||||
}
|
||||
w.logger.Info("duplicate resolve complete",
|
||||
"groups", res.Groups, "lidarr", res.LidarrConsulted, "merged", res.Merged,
|
||||
"merge_failed", res.MergeFailed, "release_changes", len(res.ReleaseChanges), "songs_linked", res.SongsLinked)
|
||||
"merge_failed", res.MergeFailed, "release_changes", len(res.ReleaseChanges), "songs_linked", res.SongsLinked,
|
||||
"held_back", res.HeldBack, "released", res.Released)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user