test-go / test (push) Failing after 44s
test-web / test (push) Successful in 49s
test-go / integration (push) Failing after 2m42s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 4m8s
Rule 25: the fingerprinting knobs move out of source into a DB-backed singleton (migration 0061), edited from a card on the Duplicates page and shared live with the scanner, the backfill and the duplicate sweep through one service instance, so a save needs no restart. The length is the knob that can silently break the library: prints taken at two lengths never match. Each track_fingerprints row now records the length it was taken at, and every reader filters on the current one — the backfill treats another length as stale, the gauge counts it pending, the sweep never streams it. Equivalent to a version bump, except that setting the length back makes rows not yet redone current again. The card warns before a length change re-fingerprints the library. Off stops every decode: the scan takes only the stream hash (a demux, and what recognises a moved file) and stores nothing, dropping a changed file's stale row; the backfill idles. A save also makes a sweep due, since a new threshold or length changes what the same prints group into, and the sweep interval gains slack so an hourly interval on an hourly tick doesn't skip every other tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: fingerprints.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const deleteTrackFingerprint = `-- name: DeleteTrackFingerprint :exec
|
|
DELETE FROM track_fingerprints WHERE track_id = $1
|
|
`
|
|
|
|
// A file changed but could not be fingerprinted, for a reason unrelated to the
|
|
// file. The stored row describes the OLD bytes, so it goes and the backfill
|
|
// re-derives it — nothing may keep trusting a stale identity.
|
|
func (q *Queries) DeleteTrackFingerprint(ctx context.Context, trackID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteTrackFingerprint, trackID)
|
|
return err
|
|
}
|
|
|
|
const getFingerprintCoverage = `-- name: GetFingerprintCoverage :one
|
|
SELECT count(*)::bigint AS total,
|
|
count(*) FILTER (
|
|
WHERE f.fingerprint_version >= $1
|
|
AND f.chromaprint_length_sec = $2
|
|
AND f.audio_stream_sha256 IS NOT NULL AND f.chromaprint IS NOT NULL
|
|
)::bigint AS fingerprinted,
|
|
count(*) FILTER (
|
|
WHERE f.fingerprint_version >= $1
|
|
AND f.chromaprint_length_sec = $2
|
|
AND (f.audio_stream_sha256 IS NULL OR f.chromaprint IS NULL)
|
|
)::bigint AS rejected,
|
|
count(*) FILTER (
|
|
WHERE f.track_id IS NULL
|
|
OR f.fingerprint_version < $1
|
|
OR f.chromaprint_length_sec <> $2
|
|
)::bigint AS pending
|
|
FROM tracks t
|
|
LEFT JOIN track_fingerprints f ON f.track_id = t.id
|
|
WHERE t.missing_since IS NULL
|
|
`
|
|
|
|
type GetFingerprintCoverageParams struct {
|
|
CurrentVersion int16
|
|
ChromaprintLengthSec int32
|
|
}
|
|
|
|
type GetFingerprintCoverageRow struct {
|
|
Total int64
|
|
Fingerprinted int64
|
|
Rejected int64
|
|
Pending int64
|
|
}
|
|
|
|
// The admin gauge for the backfill. fingerprinted + rejected + pending = total.
|
|
// "Current" means derived by the current method AT the current length: a row at
|
|
// another length is pending, because the backfill will re-derive it. rejected is
|
|
// a current row with a NULL half: a tool ran and refused the file, which is
|
|
// settled rather than waiting. Missing tracks are excluded, or the gauge could
|
|
// never reach the end.
|
|
func (q *Queries) GetFingerprintCoverage(ctx context.Context, arg GetFingerprintCoverageParams) (GetFingerprintCoverageRow, error) {
|
|
row := q.db.QueryRow(ctx, getFingerprintCoverage, arg.CurrentVersion, arg.ChromaprintLengthSec)
|
|
var i GetFingerprintCoverageRow
|
|
err := row.Scan(
|
|
&i.Total,
|
|
&i.Fingerprinted,
|
|
&i.Rejected,
|
|
&i.Pending,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listTracksNeedingFingerprint = `-- name: ListTracksNeedingFingerprint :many
|
|
SELECT t.id, t.file_path
|
|
FROM tracks t
|
|
LEFT JOIN track_fingerprints f ON f.track_id = t.id
|
|
WHERE t.missing_since IS NULL
|
|
-- A row taken at another length is as stale as one from an older method:
|
|
-- chromaprints at two lengths cannot be compared (#3913).
|
|
AND (f.track_id IS NULL
|
|
OR f.fingerprint_version < $1
|
|
OR f.chromaprint_length_sec <> $2)
|
|
AND t.id > $3
|
|
ORDER BY t.id
|
|
LIMIT $4
|
|
`
|
|
|
|
type ListTracksNeedingFingerprintParams struct {
|
|
CurrentVersion int16
|
|
ChromaprintLengthSec int32
|
|
AfterID pgtype.UUID
|
|
BatchLimit int32
|
|
}
|
|
|
|
type ListTracksNeedingFingerprintRow struct {
|
|
ID pgtype.UUID
|
|
FilePath string
|
|
}
|
|
|
|
// The backfill's work queue (#3908): tracks with no fingerprint, or one derived
|
|
// by an older method. Keyset-paged on id so a pass visits each track at most
|
|
// once. That cursor is load-bearing: an inconclusive attempt writes no row, so
|
|
// without it a file that keeps timing out would be listed again straight away
|
|
// and retried in a tight loop. Missing tracks are skipped — there is no file to
|
|
// read.
|
|
func (q *Queries) ListTracksNeedingFingerprint(ctx context.Context, arg ListTracksNeedingFingerprintParams) ([]ListTracksNeedingFingerprintRow, error) {
|
|
rows, err := q.db.Query(ctx, listTracksNeedingFingerprint,
|
|
arg.CurrentVersion,
|
|
arg.ChromaprintLengthSec,
|
|
arg.AfterID,
|
|
arg.BatchLimit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListTracksNeedingFingerprintRow
|
|
for rows.Next() {
|
|
var i ListTracksNeedingFingerprintRow
|
|
if err := rows.Scan(&i.ID, &i.FilePath); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const upsertTrackFingerprint = `-- name: UpsertTrackFingerprint :exec
|
|
INSERT INTO track_fingerprints (
|
|
track_id, audio_stream_sha256, chromaprint, fingerprint_version, chromaprint_length_sec
|
|
) VALUES (
|
|
$1, $2, $3,
|
|
$4, $5
|
|
)
|
|
ON CONFLICT (track_id) DO UPDATE SET
|
|
audio_stream_sha256 = EXCLUDED.audio_stream_sha256,
|
|
chromaprint = EXCLUDED.chromaprint,
|
|
fingerprint_version = EXCLUDED.fingerprint_version,
|
|
chromaprint_length_sec = EXCLUDED.chromaprint_length_sec,
|
|
computed_at = now()
|
|
`
|
|
|
|
type UpsertTrackFingerprintParams struct {
|
|
TrackID pgtype.UUID
|
|
AudioStreamSha256 []byte
|
|
Chromaprint []int32
|
|
FingerprintVersion int16
|
|
ChromaprintLengthSec int32
|
|
}
|
|
|
|
// Written whenever a track's fingerprint is derived: by the scan when a file is
|
|
// new or its bytes changed, and by the backfill (#3908) for rows derived by an
|
|
// older method. Replaces the row wholesale — a fingerprint of the old bytes has
|
|
// no standing once the file has changed.
|
|
func (q *Queries) UpsertTrackFingerprint(ctx context.Context, arg UpsertTrackFingerprintParams) error {
|
|
_, err := q.db.Exec(ctx, upsertTrackFingerprint,
|
|
arg.TrackID,
|
|
arg.AudioStreamSha256,
|
|
arg.Chromaprint,
|
|
arg.FingerprintVersion,
|
|
arg.ChromaprintLengthSec,
|
|
)
|
|
return err
|
|
}
|