release / web (push) Successful in 1m44s
release / go (push) Successful in 2m1s
release / govulncheck (push) Successful in 17s
release / integration (push) Successful in 5m22s
release / android (push) Successful in 5m48s
release / Build signed APK (releases and dev) (push) Successful in 5m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m6s
release / Verify release artifacts (tag releases only) (push) Skipped
Migration 0069 adds tracks.mbid_source (tag | acoustid), a lookup state per track (matched | ambiguous | no_match | failed) and the acoustid_settings row (off, no key, min score 0.85). The file's tag outranks a lookup (D4). UpsertTrack keeps a looked-up id through a re-read that finds no tag id and replaces it as soon as one appears. SetTrackMbidFromAcoustID refuses to write over a tag id. The worker fingerprints each untagged track with fpcalc's compressed print, looks it up and writes an id only when D5 settles it: one recording at or above the threshold, or one left after matching title and length. Ambiguous and no-match results write nothing. A key AcoustID refuses, or the service being unreachable, stops the pass and is reported in the worker's status. It never counts as a verdict on a track. A changed file drops its lookup in the scan. The re-lookup takes back an id that no longer matches. Admin API: GET /api/admin/library/acoustid (settings, status, coverage by source), PUT …/acoustid-settings (write-only key), POST …/acoustid/run, GET …/acoustid/unsettled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.6 KiB
SQL
102 lines
4.6 KiB
SQL
-- Duplicate merge (Scribe #3911). Every statement here runs inside the one
|
|
-- transaction library.MergeDuplicateGroup opens, after the removed copy's file
|
|
-- is already gone. The loser's own track row is deleted last with DeleteTrack;
|
|
-- what these do is move everything it carries onto the survivor first, so that
|
|
-- delete's CASCADE finds nothing left to destroy.
|
|
|
|
-- name: LockDuplicateGroupForMerge :one
|
|
-- Locks the group for the rest of the transaction, so two merges of one group
|
|
-- cannot run at once.
|
|
SELECT id, tier, status
|
|
FROM duplicate_groups
|
|
WHERE id = sqlc.arg(id)
|
|
FOR UPDATE;
|
|
|
|
-- name: ListDuplicateGroupMergeMembers :many
|
|
SELECT t.id, t.file_path, t.file_format, t.file_size, t.added_at, t.album_id,
|
|
t.mbid, albums.mbid AS album_mbid
|
|
FROM duplicate_group_members m
|
|
JOIN tracks t ON t.id = m.track_id
|
|
JOIN albums ON albums.id = t.album_id
|
|
WHERE m.group_id = sqlc.arg(group_id)
|
|
ORDER BY t.id;
|
|
|
|
-- Plain repoints: no unique key involves track_id, so moving rows cannot collide.
|
|
|
|
-- name: MergeRepointPlayEvents :execrows
|
|
UPDATE play_events SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid;
|
|
|
|
-- name: MergeRepointSkipEvents :execrows
|
|
UPDATE skip_events SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid;
|
|
|
|
-- name: MergeRepointContextualLikes :execrows
|
|
UPDATE contextual_likes SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid;
|
|
|
|
-- name: MergeRepointPlaybackErrors :execrows
|
|
UPDATE playback_errors SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid;
|
|
|
|
-- name: MergeRepointLidarrRequests :execrows
|
|
UPDATE lidarr_requests SET matched_track_id = sqlc.arg(survivor_id)::uuid
|
|
WHERE matched_track_id = sqlc.arg(loser_id)::uuid;
|
|
|
|
-- name: MergeRepointPlaylistTracks :many
|
|
-- playlist_tracks is keyed by (playlist_id, position), so repointing keeps every
|
|
-- entry exactly where it was. A playlist that held both copies simply holds the
|
|
-- survivor twice — the user put two entries there, and both stay.
|
|
UPDATE playlist_tracks SET track_id = sqlc.arg(survivor_id)::uuid
|
|
WHERE track_id = sqlc.arg(loser_id)::uuid
|
|
RETURNING playlist_id;
|
|
|
|
-- Collision-safe merges: a unique key includes track_id, so the survivor may
|
|
-- already hold a matching row. Copy what it lacks; DeleteTrack's CASCADE then
|
|
-- removes the loser's originals.
|
|
|
|
-- name: MergeCopyGeneralLikes :many
|
|
-- One like per user. A user who liked both copies keeps a single like, dated to
|
|
-- the earlier of the two.
|
|
INSERT INTO general_likes (user_id, track_id, liked_at)
|
|
SELECT user_id, sqlc.arg(survivor_id)::uuid, liked_at
|
|
FROM general_likes
|
|
WHERE track_id = sqlc.arg(loser_id)::uuid
|
|
ON CONFLICT (user_id, track_id) DO UPDATE
|
|
SET liked_at = LEAST(general_likes.liked_at, EXCLUDED.liked_at)
|
|
RETURNING user_id;
|
|
|
|
-- name: MergeCopyTrackTags :execrows
|
|
INSERT INTO track_tags (track_id, tag, weight)
|
|
SELECT sqlc.arg(survivor_id)::uuid, tag, weight
|
|
FROM track_tags
|
|
WHERE track_id = sqlc.arg(loser_id)::uuid
|
|
ON CONFLICT (track_id, tag) DO NOTHING;
|
|
|
|
-- name: MergeCopyTrackSimilarity :execrows
|
|
-- Rewrites the loser to the survivor on either side of an edge. An edge between
|
|
-- the two copies would become a track similar to itself — the table forbids
|
|
-- that, and it means nothing — so it is dropped. An edge the survivor already
|
|
-- has from the same source is kept as it is.
|
|
INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at)
|
|
SELECT CASE WHEN track_a_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_a_id END,
|
|
CASE WHEN track_b_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_b_id END,
|
|
score, source, fetched_at
|
|
FROM track_similarity
|
|
WHERE (track_a_id = sqlc.arg(loser_id)::uuid OR track_b_id = sqlc.arg(loser_id)::uuid)
|
|
AND (CASE WHEN track_a_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_a_id END)
|
|
<> (CASE WHEN track_b_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_b_id END)
|
|
ON CONFLICT (track_a_id, track_b_id, source) DO NOTHING;
|
|
|
|
-- name: MergeInheritTrackMbid :exec
|
|
-- A recording MBID is what the similarity pipeline keys on. If only the removed
|
|
-- copy carried one, the survivor takes it rather than going dark to similarity.
|
|
UPDATE tracks AS survivor
|
|
SET mbid = loser.mbid, mbid_source = loser.mbid_source
|
|
FROM tracks AS loser
|
|
WHERE survivor.id = sqlc.arg(survivor_id)::uuid
|
|
AND loser.id = sqlc.arg(loser_id)::uuid
|
|
AND survivor.mbid IS NULL
|
|
AND loser.mbid IS NOT NULL;
|
|
|
|
-- name: MarkDuplicateGroupMerged :execrows
|
|
UPDATE duplicate_groups
|
|
SET status = 'merged', resolved_at = now()
|
|
WHERE id = sqlc.arg(id) AND status = 'pending';
|