-- name: StartDuplicateSweep :one INSERT INTO duplicate_sweeps DEFAULT VALUES RETURNING id, started_at; -- name: FinishDuplicateSweep :exec UPDATE duplicate_sweeps SET finished_at = now(), candidates = sqlc.arg(candidates), groups_found = sqlc.arg(groups_found), oversize_clusters = sqlc.arg(oversize_clusters), error_message = NULLIF(sqlc.arg(error_message)::text, '') WHERE id = sqlc.arg(id); -- name: GetInFlightDuplicateSweep :one -- The guard against two sweeps at once: "in flight" is finished_at IS NULL. SELECT id, started_at FROM duplicate_sweeps WHERE finished_at IS NULL ORDER BY started_at DESC LIMIT 1; -- name: GetLatestDuplicateSweep :one SELECT id, started_at, finished_at, candidates, groups_found, oversize_clusters, error_message FROM duplicate_sweeps ORDER BY started_at DESC LIMIT 1; -- name: GetLatestFingerprintComputedAt :one -- Whether a sweep has anything new to look at: fingerprints written since the -- last sweep started. SELECT max(computed_at)::timestamptz AS latest FROM track_fingerprints; -- name: ListExactDuplicateHashes :many -- The exact tier, library-wide in one pass: identical encoded audio shared by -- more than one present track. SELECT f.audio_stream_sha256, array_agg(t.id ORDER BY t.id)::uuid[] AS track_ids FROM track_fingerprints f JOIN tracks t ON t.id = f.track_id WHERE t.missing_since IS NULL AND f.fingerprint_version >= sqlc.arg(current_version) AND f.audio_stream_sha256 IS NOT NULL GROUP BY f.audio_stream_sha256 HAVING count(*) > 1; -- name: ListDuplicateCandidates :many -- The acoustic tier's input, one page at a time in (duration_ms, id) order so the -- sweep holds only a sliding window of durations. Tracks without a chromaprint -- cannot be compared acoustically and are left out; any exact duplicates among -- them come from ListExactDuplicateHashes. SELECT t.id, t.duration_ms, f.audio_stream_sha256, f.chromaprint FROM tracks t JOIN track_fingerprints f ON f.track_id = t.id WHERE t.missing_since IS NULL AND f.fingerprint_version >= sqlc.arg(current_version) AND f.chromaprint IS NOT NULL -- Only chromaprints taken at the current length: prints at two lengths are not -- comparable, and after a length change the backfill is still re-deriving the -- rest (#3913). AND f.chromaprint_length_sec = sqlc.arg(chromaprint_length_sec) AND (t.duration_ms, t.id) > (sqlc.arg(after_duration_ms)::integer, sqlc.arg(after_id)::uuid) ORDER BY t.duration_ms, t.id LIMIT sqlc.arg(page_limit); -- name: ListDismissedDuplicateMemberSets :many -- What the operator has already said are not duplicates. A new proposal whose -- every member sat together in one of these is not proposed again. SELECT g.id, array_agg(m.track_id ORDER BY m.track_id)::uuid[] AS track_ids FROM duplicate_groups g JOIN duplicate_group_members m ON m.group_id = g.id WHERE g.status = 'dismissed' GROUP BY g.id; -- name: UpsertDuplicateGroup :one -- Proposes a group, or refreshes one already pending. A group already dismissed -- or merged is left exactly as it is: the WHERE on the update makes the conflict -- a no-op, and the caller sees no row. INSERT INTO duplicate_groups (member_key, tier, worst_bit_error_rate, last_seen_sweep_id) VALUES (sqlc.arg(member_key), sqlc.arg(tier), sqlc.narg(worst_bit_error_rate), sqlc.arg(sweep_id)) ON CONFLICT (member_key) DO UPDATE SET tier = EXCLUDED.tier, worst_bit_error_rate = EXCLUDED.worst_bit_error_rate, last_seen_sweep_id = EXCLUDED.last_seen_sweep_id WHERE duplicate_groups.status = 'pending' RETURNING id; -- name: AddDuplicateGroupMember :exec INSERT INTO duplicate_group_members (group_id, track_id) VALUES (sqlc.arg(group_id), sqlc.arg(track_id)) ON CONFLICT DO NOTHING; -- name: DeleteStalePendingDuplicateGroups :execrows -- A pending proposal this sweep did not find again no longer describes the -- library: a member was re-fingerprinted, merged away or went missing. Dismissed -- groups are kept regardless — they are the memory of a decision. -- -- Only proposals last confirmed by an EARLIER sweep go. Should two sweeps ever -- overlap (a manual trigger racing the worker), neither may delete what the other -- has just found. DELETE FROM duplicate_groups g WHERE g.status = 'pending' AND g.last_seen_sweep_id IS DISTINCT FROM sqlc.arg(sweep_id) AND (g.last_seen_sweep_id IS NULL OR (SELECT s.started_at FROM duplicate_sweeps s WHERE s.id = g.last_seen_sweep_id) < (SELECT s.started_at FROM duplicate_sweeps s WHERE s.id = sqlc.arg(sweep_id))); -- name: CountPendingDuplicateGroups :one -- Proposals awaiting review. A group left with one member — its other tracks -- deleted since the sweep — is no proposal at all and is not counted; the next -- sweep retires it. SELECT count(*)::bigint FROM duplicate_groups g WHERE g.status = 'pending' AND (SELECT count(*) FROM duplicate_group_members m WHERE m.group_id = g.id) >= 2; -- name: CountPendingDuplicateGroupsInView :one -- CountPendingDuplicateGroups narrowed to one view of the report (M498): -- review what the operator still has to decide: every class but -- cross_release, and groups not yet classified -- cross_release the same song on different releases, which is kept, not merged SELECT count(*)::bigint FROM duplicate_groups g WHERE g.status = 'pending' AND (SELECT count(*) FROM duplicate_group_members m WHERE m.group_id = g.id) >= 2 AND CASE sqlc.arg(view)::text WHEN 'cross_release' THEN g.class = 'cross_release' ELSE g.class IS DISTINCT FROM 'cross_release' END; -- name: ListPendingDuplicateGroupMembers :many -- One page of proposals in one view (see CountPendingDuplicateGroupsInView), -- newest first, flattened to one row per member so the handler folds them -- without a query per group. What each copy carries — likes and plays from -- every user — is here because it is what the operator weighs when deciding -- which copy to keep; Lidarr's view of it, because a copy Lidarr tracks cannot -- be removed without Lidarr downloading it again. WITH page AS ( SELECT g.id, g.tier, g.worst_bit_error_rate, g.detected_at, g.class, g.resolve_note FROM duplicate_groups g WHERE g.status = 'pending' AND (SELECT count(*) FROM duplicate_group_members m WHERE m.group_id = g.id) >= 2 AND CASE sqlc.arg(view)::text WHEN 'cross_release' THEN g.class = 'cross_release' ELSE g.class IS DISTINCT FROM 'cross_release' END ORDER BY g.detected_at DESC, g.id LIMIT sqlc.arg(page_limit) OFFSET sqlc.arg(page_offset) ) SELECT p.id AS group_id, p.tier, p.worst_bit_error_rate, p.detected_at, p.class, p.resolve_note, m.lidarr_state, t.disc_number, t.track_number, (t.mbid IS NOT NULL)::boolean AS has_mbid, EXISTS (SELECT 1 FROM tracks o WHERE o.album_id = t.album_id AND o.id <> t.id AND o.missing_since IS NULL AND t.track_number IS NOT NULL AND o.disc_number IS NOT DISTINCT FROM t.disc_number AND o.track_number = t.track_number)::boolean AS position_clash, t.id AS track_id, t.title, artists.name AS artist_name, albums.id AS album_id, albums.title AS album_title, t.file_path, t.file_format, t.file_size, t.duration_ms, t.added_at, (SELECT count(*) FROM general_likes l WHERE l.track_id = t.id)::bigint AS like_count, (SELECT count(*) FROM play_events e WHERE e.track_id = t.id)::bigint AS play_count FROM page p JOIN duplicate_group_members m ON m.group_id = p.id JOIN tracks t ON t.id = m.track_id JOIN albums ON albums.id = t.album_id JOIN artists ON artists.id = t.artist_id ORDER BY p.detected_at DESC, p.id, t.id; -- name: DismissDuplicateGroup :execrows -- "These are not duplicates." Only a pending group can be dismissed; zero rows -- means it was already resolved or no longer exists. UPDATE duplicate_groups SET status = 'dismissed', resolved_at = now() WHERE id = sqlc.arg(id) AND status = 'pending'; -- name: CountDuplicateGroupsDetectedSince :one -- Pending proposals first made at or after `since`: what a sweep that started -- then found for the first time. A refreshed proposal keeps its detected_at, -- so a sweep that only re-finds known groups counts none (M489: admins are -- told about new duplicates, not reminded of the same ones every sweep). SELECT count(*)::bigint FROM duplicate_groups WHERE status = 'pending' AND detected_at >= sqlc.arg(since); -- name: ListDuplicateGroupsForResolve :many -- Every pending group's present copies, for the resolver (M498 #5435): what it -- needs to classify the group and to choose the copy to keep. One row per copy, -- grouped by group. position_clash is true when another present track on the -- same album claims the same disc and track number: tags that do not fit the -- album, which the survivor rule weighs against a copy. SELECT g.id AS group_id, g.tier, t.id AS track_id, t.album_id, t.title, artists.name AS artist_name, t.file_path, t.file_format, t.file_size, t.added_at, t.disc_number, t.track_number, (t.mbid IS NOT NULL)::boolean AS has_mbid, albums.release_group_mbid, albums.title AS album_title, EXISTS (SELECT 1 FROM tracks o WHERE o.album_id = t.album_id AND o.id <> t.id AND o.missing_since IS NULL AND t.track_number IS NOT NULL AND o.disc_number IS NOT DISTINCT FROM t.disc_number AND o.track_number = t.track_number)::boolean AS position_clash FROM duplicate_groups g JOIN duplicate_group_members m ON m.group_id = g.id JOIN tracks t ON t.id = m.track_id JOIN albums ON albums.id = t.album_id JOIN artists ON artists.id = t.artist_id WHERE g.status = 'pending' AND t.missing_since IS NULL ORDER BY g.id, t.id; -- name: SetDuplicateGroupClasses :exec -- The resolver's verdicts, written in one statement. Only rows whose class or -- note actually changed are touched, so an hourly pass over thousands of -- unchanged groups writes nothing. An empty note clears it. UPDATE duplicate_groups g SET class = v.class, resolve_note = NULLIF(v.note, '') FROM (SELECT unnest(sqlc.arg(group_ids)::uuid[]) AS id, unnest(sqlc.arg(classes)::text[]) AS class, unnest(sqlc.arg(notes)::text[]) AS note) v WHERE g.id = v.id AND g.status = 'pending' AND (g.class IS DISTINCT FROM v.class OR g.resolve_note IS DISTINCT FROM NULLIF(v.note, '')); -- name: SetDuplicateMemberLidarrStates :exec -- Lidarr's view of each copy as of this pass; an empty state clears it (Lidarr -- not consulted). Unchanged rows are left alone. UPDATE duplicate_group_members m SET lidarr_state = NULLIF(v.state, '') FROM (SELECT unnest(sqlc.arg(group_ids)::uuid[]) AS group_id, unnest(sqlc.arg(track_ids)::uuid[]) AS track_id, unnest(sqlc.arg(states)::text[]) AS state) v WHERE m.group_id = v.group_id AND m.track_id = v.track_id AND m.lidarr_state IS DISTINCT FROM NULLIF(v.state, ''); -- name: MarkDuplicateGroupResolvedAutomatically :exec UPDATE duplicate_groups SET resolved_automatically = true WHERE id = sqlc.arg(id); -- name: ListAlbumPresentTrackTitles :many -- The titles an album has on disk, for choosing the Lidarr release that covers -- them best (M498 #5437). SELECT title FROM tracks WHERE album_id = sqlc.arg(album_id) AND missing_since IS NULL;