Files
minstrel/internal/db/dbq/missing_pairs.sql.go
T
bvandeusenandClaude Opus 5.5 3ebd35e51d
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m28s
release / go (push) Successful in 1m36s
release / android (push) Successful in 5m2s
release / integration (push) Failing after 5m7s
release / Build signed APK (releases and dev) (push) Successful in 5m12s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
fix: a slot the resolver emptied folds only into a copy of its own song (M498)
On 2026-10-20 the emptied "Open the Gateway" slot of Auvic's Speed Demons
was folded by title into another file tagged "Open the Gateway" that holds
Onward (244 s against the song's 254 s), so the album was never wanted
back. A wrong-slot removal now stamps tracks.emptied_at (migration 0095)
when the album holds no copy of the song as long as its MusicBrainz slot,
and missing-pair folding pairs such a row by title only with a track added
since: a copy fetched again. A slot whose song the album does hold (VOID
DLC's Weeper beside Void's) folds into it as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:55:23 -04:00

159 lines
5.4 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: missing_pairs.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const listMissingTrackPairs = `-- name: ListMissingTrackPairs :many
WITH grp AS (
SELECT id AS album_id, COALESCE(release_group_mbid, id::text) AS g
FROM albums
),
cand AS (
SELECT m.id AS missing_id, p.id AS present_id, 'mbid'::text AS rule
FROM tracks m
JOIN grp gm ON gm.album_id = m.album_id
JOIN grp gp ON gp.g = gm.g
JOIN tracks p ON p.album_id = gp.album_id
AND p.missing_since IS NULL
AND p.mbid = m.mbid
WHERE m.missing_since IS NOT NULL
AND m.mbid IS NOT NULL
UNION ALL
SELECT m.id, p.id, 'title'::text
FROM tracks m
JOIN tracks p ON p.album_id = m.album_id
AND p.missing_since IS NULL
AND lower(p.title) = lower(m.title)
WHERE m.missing_since IS NOT NULL
AND (m.mbid IS NULL OR p.mbid IS NULL OR m.mbid = p.mbid)
AND (m.emptied_at IS DISTINCT FROM m.missing_since OR p.added_at > m.emptied_at)
),
pairs AS (
-- A pair both rules found is one candidate, credited to the stronger rule.
SELECT missing_id, present_id, min(rule)::text AS rule
FROM cand
GROUP BY missing_id, present_id
)
SELECT pr.missing_id, pr.present_id, pr.rule,
m.file_path AS missing_path, p.file_path AS present_path,
(count(*) OVER (PARTITION BY pr.missing_id))::int AS missing_candidates,
(count(*) OVER (PARTITION BY pr.present_id))::int AS present_candidates
FROM pairs pr
JOIN tracks m ON m.id = pr.missing_id
JOIN tracks p ON p.id = pr.present_id
ORDER BY m.file_path, p.file_path
`
type ListMissingTrackPairsRow struct {
MissingID pgtype.UUID
PresentID pgtype.UUID
Rule string
MissingPath string
PresentPath string
MissingCandidates int32
PresentCandidates int32
}
// Missing-pair pass (M485). A track marked missing whose replacement is already
// on disk under another row — most often a quality upgrade, mp3 → flac at a new
// path — is folded into that replacement, so its history moves across and
// re-acquisition stops chasing music the library already has.
//
// Move adoption (#2528) cannot catch these: it links a new file to a missing row
// at the moment the file is first inserted, and the replacement arrives as a
// re-encode with no recording MBID, so it has nothing to match on. AcoustID may
// give it an MBID later; by then both rows exist and only a fold can join them.
// Every candidate (missing, present) pair, with how many candidates each side
// has. Two rules, measured on the operator's library on 2026-10-07 (#5274):
//
// mbid the same recording MBID, within the album group (the album row, or
// albums sharing a release group). 142 of 359 pairs.
// title the same album row and the same title, ignoring case. 213 more. The
// replacements are often not the original file — another edition,
// re-encoded, untagged — so the metadata is what is left to go on.
//
// Deliberately NOT gated on duration or track position. On the 142 pairs known
// to be one recording, 18% differed by over 2s (old mp3 durations are poor
// estimates), and the poorly tagged set is exactly where numbering is broken.
//
// A pair whose MBIDs are both set and differ is not a title match: the tags say
// they are different recordings. The caller folds a pair only when each side has
// exactly one candidate; anything else is ambiguous and left alone.
//
// A slot the duplicate resolver emptied (emptied_at, migration 0095) pairs by
// title only with a track added since: the album held no copy of the song as
// long as it is, so one already there under the title is another song.
func (q *Queries) ListMissingTrackPairs(ctx context.Context) ([]ListMissingTrackPairsRow, error) {
rows, err := q.db.Query(ctx, listMissingTrackPairs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListMissingTrackPairsRow
for rows.Next() {
var i ListMissingTrackPairsRow
if err := rows.Scan(
&i.MissingID,
&i.PresentID,
&i.Rule,
&i.MissingPath,
&i.PresentPath,
&i.MissingCandidates,
&i.PresentCandidates,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockTracksForPairing = `-- name: LockTracksForPairing :many
SELECT id, missing_since
FROM tracks
WHERE id = ANY($1::uuid[])
ORDER BY id
FOR UPDATE
`
type LockTracksForPairingRow struct {
ID pgtype.UUID
MissingSince pgtype.Timestamptz
}
// Locks both rows of a pair for the fold's transaction, in id order so two
// passes cannot deadlock, and returns what the fold re-checks: the missing row
// must still be missing and the present one still present. A scan may have
// restored or adopted the first, or marked the second, since the pairs were listed.
func (q *Queries) LockTracksForPairing(ctx context.Context, ids []pgtype.UUID) ([]LockTracksForPairingRow, error) {
rows, err := q.db.Query(ctx, lockTracksForPairing, ids)
if err != nil {
return nil, err
}
defer rows.Close()
var items []LockTracksForPairingRow
for rows.Next() {
var i LockTracksForPairingRow
if err := rows.Scan(&i.ID, &i.MissingSince); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}