release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
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
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz release group. A want comes from an approved request, a new release by a followed artist, or an album whose files went missing, and is held once the library has a full edition of it. - Migration 0081: release_type_profiles (Standard seeded: Album, EP, Single, studio only), follows, follow_seen_groups and wants. lidarr_requests becomes requests with plain *_mbid columns; approved album and track requests move onto the wanted list, approved artist requests become follows, and the library's album artists start out followed. missing_reacquisitions and the retry settings go. - internal/wanted: follows are checked against MusicBrainz discographies daily. The first check sets a baseline, and only releases after it are wanted unless the follow asked for what already exists. Profiles filter by primary and secondary type, so live recordings stay out unless a profile ticks Live. - internal/requests (was lidarrrequests): approving puts the request on the wanted list, and an artist request can be approved on a chosen profile. A request completes when its want is held. - The re-acquisition sweeper wants a missing album back instead of asking Lidarr; searching for it is the search schedule's (#5521). - Web: an admin Wanted page (Wanted, Missing files, Held, Following), a follow control on artist pages, a release-type profiles card on Integrations, "Approve on a profile" on Requests, and want state on Missing files. The artist detail API returns the artist's MBID. - Android: the request body sends artist_mbid, album_mbid and track_mbid. 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: MergeRepointRequests :execrows
|
|
UPDATE 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';
|