The duplicate sweep proposed 4,197 groups and every one waited for the
operator. Most are safe to settle, and Lidarr defines what safe means: it
maps one file to each track of the release it monitors and downloads any
mapped file that disappears. Deleting a mapped copy opens exactly the hole
the operator saw Lidarr fill.
Classify (#5435)
- Migration 0075: duplicate_groups.class (same_release, cross_release,
mismatch, review), resolve_note, resolved_automatically;
duplicate_group_members.lidarr_state (tracked, unmapped);
fingerprint_settings.auto_resolve; notification kind
duplicates_resolved with both kind CHECKs swapped (rule 36).
- library.ClassifyDuplicateGroup, with MatchTitleKey dropping featuring
credits, remaster notes and video-rip markers, and keeping live, demo,
remix and instrumental. The rip markers move from api to library.
Choose the copy to keep (#5436)
- ProposeSurvivor ranks the copy Lidarr maps first, then tag fit (a
clash-free track number, no rip marker in the name, an MBID), then the
quality rules. File size picked the wrong Humanz copy in 6 of 21 groups.
Act (#5437)
- An hourly resolver pass reads Lidarr's unmapped files, matched by the
last three path components, and records each copy's state.
- Same album, with at most one copy mapped: merged into the mapped copy.
The merge is guarded, so a mapped copy can never be removed
(MergeDuplicateGroupGuarded, ErrCopyTrackedByLidarr).
- Same album, every copy mapped: the monitored release lists the song
twice (Humanz's 14x12" box set). The pass moves Lidarr to the release
that lists each song once and best covers what is on disk. It never
picks one covering less, and is capped at 10 albums per pass.
- Fixed point (lesson #4183): the chosen release no longer repeats.
- The album is left alone for 24h while Lidarr rescans, so "every copy
unmapped" mid-rescan is never read as licence to merge.
- Both actions are audited with no actor and summarised to admins. The
operator can switch them off in the Fingerprinting card (rule 25).
- Manual merges use the same guard: 409 copy_tracked_by_lidarr, or 503
lidarr_unavailable when Lidarr cannot say.
Web
- Duplicates gets tabs: Needs review, Across releases, Resolved
automatically. Each loads as you scroll (rule 172), replacing the
pager.
- Each copy says whether Lidarr uses it.
- The resolver's note shows on each group.
- The merge confirm blocks, before sending, a merge that would remove
the copy Lidarr uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema for user management U1: display_name on users; user_invites;
registration_settings singleton (default 'invite_only'); audit_log.
Plus the internal/audit package centralizing the action-name
vocabulary and JSON metadata marshaling so handlers don't repeat
boilerplate.
Race-safe first-admin uses a query-shape primitive
(CreateUserFirstAdminRace's WHERE NOT EXISTS subquery) rather than
a schema-level constraint. Concurrent empty-state registrations
both see 'no users yet' and both insert as admin — fine, having
two admins from the start is benign; what matters is at-least-one.
users.username uniqueness arbitrates if the two callers picked the
same username.
CreateUser signature gains display_name (nullable); existing
bootstrap call sites pass nil. The audit package declares the full
U1+U2+U3 action vocabulary upfront so subsequent slices are purely
additive on the caller side.
Tests cover audit Write with + without metadata and that every
declared action constant persists correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>