docs: library comments say what the scanner and resolver do, not how they came to be (#6277)
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped

Rewrites the internal/library comment blocks to rule 32. Task, milestone,
decision and rule ids are gone; the incidents behind them are restated as
the constraint they left (why a file is deleted before its row, why the
missing mark is a mark, why duplicates block on duration). Counts and
album names from one library are dropped from production comments; test
comments keep the names their fixtures use. The tagReadVersion log keeps
each version's before and after, without the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 12:27:34 -04:00
co-authored by Claude Opus 5.5
parent 08a2c06b85
commit 7e6228c13f
66 changed files with 578 additions and 564 deletions
+20 -18
View File
@@ -16,13 +16,14 @@ import (
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
)
// Duplicate merge (M400 #3911).
// Duplicate merge.
// ErrDuplicateGroupNotPending means the group was already merged or dismissed,
// no longer exists, or no longer has two members to merge.
var ErrDuplicateGroupNotPending = errors.New("library: duplicate group is not pending")
// ErrSurvivorNotInGroup means the copy chosen to keep is not a member of the group.
// ErrSurvivorNotInGroup means the copy chosen to keep is not a member of the
// group.
var ErrSurvivorNotInGroup = errors.New("library: survivor is not a member of the group")
// MergedCopy is one copy a merge kept or removed.
@@ -37,7 +38,7 @@ type MergeResult struct {
Survivor MergedCopy
Removed []MergedCopy
// What moved onto the survivor — reported so the operator, and the audit
// What moved onto the survivor — reported so an admin, and the audit
// log, can see that the history was kept rather than take it on trust.
PlayEvents int64
SkipEvents int64
@@ -60,16 +61,16 @@ type MergeResult struct {
// survivor first, and only then deletes the now-empty row.
//
// It deletes the removed copies' FILES too, and first, before any row changes
// (#3918, note #3926). A merge that left the file behind would be undone by the
// next scan, which re-imports it as a new track with no history. If a file cannot
// be removed, the *FileRemoveError comes back and nothing in the database changes.
// With several copies to remove, one file may already be gone when a later one
// fails; that copy's row keeps all its history and is marked missing by the next
// scan, and retrying the merge picks up where it stopped.
// (DeleteTrackFile's order). A merge that left the file behind would be undone
// by the next scan, which re-imports it as a new track with no history. If a
// file cannot be removed, the *FileRemoveError comes back and nothing in the
// database changes. With several copies to remove, one file may already be gone
// when a later one fails; that copy's row keeps all its history and is marked
// missing by the next scan, and retrying the merge picks up where it stopped.
//
// Everything else happens in one transaction, which holds a lock on the group so
// two merges of it cannot run at once. Sync changes for clients' caches are logged
// inside it, the way the playlists service logs its own.
// Everything else happens in one transaction, which holds a lock on the group
// so two merges of it cannot run at once. Sync changes for clients' caches are
// logged inside it, the way the playlists service logs its own.
func MergeDuplicateGroup(
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string,
groupID, survivorID pgtype.UUID,
@@ -152,7 +153,8 @@ func MergeDuplicateGroup(
return MergeResult{}, fmt.Errorf("commit merge: %w", err)
}
// After commit, like DeleteTrackFile: a leftover art directory is only disk.
// After commit, like DeleteTrackFile: a leftover art directory is only
// disk.
if dataDir != "" {
for _, artistID := range res.DeletedArtistIDs {
if err := coverart.CleanupArtistArt(dataDir, artistID); err != nil {
@@ -176,11 +178,11 @@ type foldResult struct {
}
// foldTrackInto moves everything the loser row carries onto the survivor, then
// deletes the loser row and tidies an album or artist it leaves empty. It is the
// row half of a merge, shared by the duplicate merge and the missing-pair pass
// (M485): neither touches a file here, and both run it inside their own
// transaction. The sync changes it owes clients go into changes, which the
// caller logs once before committing.
// deletes the loser row and tidies an album or artist it leaves empty. It is
// the row half of a merge, shared by the duplicate merge and the missing-pair
// pass (PairMissingTracks): neither touches a file here, and both run it inside
// their own transaction. The sync changes it owes clients go into changes,
// which the caller logs once before committing.
func foldTrackInto(
ctx context.Context, tq *dbq.Queries, survivorID, loserID pgtype.UUID, changes *mergeChanges,
) (foldResult, error) {