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>
340 lines
10 KiB
Go
340 lines
10 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: merge.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const listDuplicateGroupMergeMembers = `-- 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 = $1
|
|
ORDER BY t.id
|
|
`
|
|
|
|
type ListDuplicateGroupMergeMembersRow struct {
|
|
ID pgtype.UUID
|
|
FilePath string
|
|
FileFormat string
|
|
FileSize int64
|
|
AddedAt pgtype.Timestamptz
|
|
AlbumID pgtype.UUID
|
|
Mbid *string
|
|
AlbumMbid *string
|
|
}
|
|
|
|
func (q *Queries) ListDuplicateGroupMergeMembers(ctx context.Context, groupID pgtype.UUID) ([]ListDuplicateGroupMergeMembersRow, error) {
|
|
rows, err := q.db.Query(ctx, listDuplicateGroupMergeMembers, groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListDuplicateGroupMergeMembersRow
|
|
for rows.Next() {
|
|
var i ListDuplicateGroupMergeMembersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.FilePath,
|
|
&i.FileFormat,
|
|
&i.FileSize,
|
|
&i.AddedAt,
|
|
&i.AlbumID,
|
|
&i.Mbid,
|
|
&i.AlbumMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockDuplicateGroupForMerge = `-- name: LockDuplicateGroupForMerge :one
|
|
|
|
SELECT id, tier, status
|
|
FROM duplicate_groups
|
|
WHERE id = $1
|
|
FOR UPDATE
|
|
`
|
|
|
|
type LockDuplicateGroupForMergeRow struct {
|
|
ID pgtype.UUID
|
|
Tier string
|
|
Status string
|
|
}
|
|
|
|
// 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.
|
|
// Locks the group for the rest of the transaction, so two merges of one group
|
|
// cannot run at once.
|
|
func (q *Queries) LockDuplicateGroupForMerge(ctx context.Context, id pgtype.UUID) (LockDuplicateGroupForMergeRow, error) {
|
|
row := q.db.QueryRow(ctx, lockDuplicateGroupForMerge, id)
|
|
var i LockDuplicateGroupForMergeRow
|
|
err := row.Scan(&i.ID, &i.Tier, &i.Status)
|
|
return i, err
|
|
}
|
|
|
|
const markDuplicateGroupMerged = `-- name: MarkDuplicateGroupMerged :execrows
|
|
UPDATE duplicate_groups
|
|
SET status = 'merged', resolved_at = now()
|
|
WHERE id = $1 AND status = 'pending'
|
|
`
|
|
|
|
func (q *Queries) MarkDuplicateGroupMerged(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, markDuplicateGroupMerged, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeCopyGeneralLikes = `-- name: MergeCopyGeneralLikes :many
|
|
|
|
INSERT INTO general_likes (user_id, track_id, liked_at)
|
|
SELECT user_id, $1::uuid, liked_at
|
|
FROM general_likes
|
|
WHERE track_id = $2::uuid
|
|
ON CONFLICT (user_id, track_id) DO UPDATE
|
|
SET liked_at = LEAST(general_likes.liked_at, EXCLUDED.liked_at)
|
|
RETURNING user_id
|
|
`
|
|
|
|
type MergeCopyGeneralLikesParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
// One like per user. A user who liked both copies keeps a single like, dated to
|
|
// the earlier of the two.
|
|
func (q *Queries) MergeCopyGeneralLikes(ctx context.Context, arg MergeCopyGeneralLikesParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, mergeCopyGeneralLikes, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []pgtype.UUID
|
|
for rows.Next() {
|
|
var user_id pgtype.UUID
|
|
if err := rows.Scan(&user_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, user_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const mergeCopyTrackSimilarity = `-- name: MergeCopyTrackSimilarity :execrows
|
|
INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at)
|
|
SELECT CASE WHEN track_a_id = $1::uuid THEN $2::uuid ELSE track_a_id END,
|
|
CASE WHEN track_b_id = $1::uuid THEN $2::uuid ELSE track_b_id END,
|
|
score, source, fetched_at
|
|
FROM track_similarity
|
|
WHERE (track_a_id = $1::uuid OR track_b_id = $1::uuid)
|
|
AND (CASE WHEN track_a_id = $1::uuid THEN $2::uuid ELSE track_a_id END)
|
|
<> (CASE WHEN track_b_id = $1::uuid THEN $2::uuid ELSE track_b_id END)
|
|
ON CONFLICT (track_a_id, track_b_id, source) DO NOTHING
|
|
`
|
|
|
|
type MergeCopyTrackSimilarityParams struct {
|
|
LoserID pgtype.UUID
|
|
SurvivorID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) MergeCopyTrackSimilarity(ctx context.Context, arg MergeCopyTrackSimilarityParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeCopyTrackSimilarity, arg.LoserID, arg.SurvivorID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeCopyTrackTags = `-- name: MergeCopyTrackTags :execrows
|
|
INSERT INTO track_tags (track_id, tag, weight)
|
|
SELECT $1::uuid, tag, weight
|
|
FROM track_tags
|
|
WHERE track_id = $2::uuid
|
|
ON CONFLICT (track_id, tag) DO NOTHING
|
|
`
|
|
|
|
type MergeCopyTrackTagsParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) MergeCopyTrackTags(ctx context.Context, arg MergeCopyTrackTagsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeCopyTrackTags, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeInheritTrackMbid = `-- name: MergeInheritTrackMbid :exec
|
|
UPDATE tracks AS survivor
|
|
SET mbid = loser.mbid, mbid_source = loser.mbid_source
|
|
FROM tracks AS loser
|
|
WHERE survivor.id = $1::uuid
|
|
AND loser.id = $2::uuid
|
|
AND survivor.mbid IS NULL
|
|
AND loser.mbid IS NOT NULL
|
|
`
|
|
|
|
type MergeInheritTrackMbidParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) MergeInheritTrackMbid(ctx context.Context, arg MergeInheritTrackMbidParams) error {
|
|
_, err := q.db.Exec(ctx, mergeInheritTrackMbid, arg.SurvivorID, arg.LoserID)
|
|
return err
|
|
}
|
|
|
|
const mergeRepointContextualLikes = `-- name: MergeRepointContextualLikes :execrows
|
|
UPDATE contextual_likes SET track_id = $1::uuid WHERE track_id = $2::uuid
|
|
`
|
|
|
|
type MergeRepointContextualLikesParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) MergeRepointContextualLikes(ctx context.Context, arg MergeRepointContextualLikesParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeRepointContextualLikes, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeRepointLidarrRequests = `-- name: MergeRepointLidarrRequests :execrows
|
|
UPDATE lidarr_requests SET matched_track_id = $1::uuid
|
|
WHERE matched_track_id = $2::uuid
|
|
`
|
|
|
|
type MergeRepointLidarrRequestsParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) MergeRepointLidarrRequests(ctx context.Context, arg MergeRepointLidarrRequestsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeRepointLidarrRequests, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeRepointPlayEvents = `-- name: MergeRepointPlayEvents :execrows
|
|
|
|
UPDATE play_events SET track_id = $1::uuid WHERE track_id = $2::uuid
|
|
`
|
|
|
|
type MergeRepointPlayEventsParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
// Plain repoints: no unique key involves track_id, so moving rows cannot collide.
|
|
func (q *Queries) MergeRepointPlayEvents(ctx context.Context, arg MergeRepointPlayEventsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeRepointPlayEvents, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeRepointPlaybackErrors = `-- name: MergeRepointPlaybackErrors :execrows
|
|
UPDATE playback_errors SET track_id = $1::uuid WHERE track_id = $2::uuid
|
|
`
|
|
|
|
type MergeRepointPlaybackErrorsParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) MergeRepointPlaybackErrors(ctx context.Context, arg MergeRepointPlaybackErrorsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeRepointPlaybackErrors, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const mergeRepointPlaylistTracks = `-- name: MergeRepointPlaylistTracks :many
|
|
UPDATE playlist_tracks SET track_id = $1::uuid
|
|
WHERE track_id = $2::uuid
|
|
RETURNING playlist_id
|
|
`
|
|
|
|
type MergeRepointPlaylistTracksParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) MergeRepointPlaylistTracks(ctx context.Context, arg MergeRepointPlaylistTracksParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, mergeRepointPlaylistTracks, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []pgtype.UUID
|
|
for rows.Next() {
|
|
var playlist_id pgtype.UUID
|
|
if err := rows.Scan(&playlist_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, playlist_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const mergeRepointSkipEvents = `-- name: MergeRepointSkipEvents :execrows
|
|
UPDATE skip_events SET track_id = $1::uuid WHERE track_id = $2::uuid
|
|
`
|
|
|
|
type MergeRepointSkipEventsParams struct {
|
|
SurvivorID pgtype.UUID
|
|
LoserID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) MergeRepointSkipEvents(ctx context.Context, arg MergeRepointSkipEventsParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, mergeRepointSkipEvents, arg.SurvivorID, arg.LoserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|