Files
minstrel/internal/db/queries/loudness.sql
T
bvandeusenandClaude Opus 5.5 e3aa8629d3
release / web (push) Successful in 1m44s
release / go (push) Successful in 2m12s
release / govulncheck (push) Successful in 40s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 4m42s
release / integration (push) Successful in 15m33s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m21s
release / Verify release artifacts (tag releases only) (push) Skipped
feat(api): deliver loudness gains to every client (M464 #4997)
ReplayGain 2.0 values (gain to -18 LUFS, linear peak) derived from the
stored track and album loudness:

- Web: GET /api/tracks/replay-gain?ids=... (up to 200), a lookup the
  player calls for its queue, rather than a field on every TrackRef
  surface.
- Android: track_gain/track_peak and album_gain/album_peak on the sync
  views, so cached tracks level offline. Storing a measurement logs a
  track change, and an album's values moving logs an album change, both
  before the write (#2704), so caches pick the gains up.
- OpenSubsonic: replayGain on every song (album, getSong, search3,
  starred), as a JSON object and an XML element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:54:05 -04:00

172 lines
7.1 KiB
SQL

-- name: UpsertTrackLoudness :exec
-- Written when the backfill measures a track (#4995). Replaces the row
-- wholesale: a measurement of the old bytes has no standing once the file has
-- changed.
INSERT INTO track_loudness (
track_id, integrated_lufs, true_peak_dbtp, loudness_range_lu,
block_hist_start, block_hist, unreadable, analysis_version
) VALUES (
sqlc.arg(track_id), sqlc.narg(integrated_lufs), sqlc.narg(true_peak_dbtp),
sqlc.narg(loudness_range_lu), sqlc.narg(block_hist_start), sqlc.narg(block_hist),
sqlc.arg(unreadable), sqlc.arg(analysis_version)
)
ON CONFLICT (track_id) DO UPDATE SET
integrated_lufs = EXCLUDED.integrated_lufs,
true_peak_dbtp = EXCLUDED.true_peak_dbtp,
loudness_range_lu = EXCLUDED.loudness_range_lu,
block_hist_start = EXCLUDED.block_hist_start,
block_hist = EXCLUDED.block_hist,
unreadable = EXCLUDED.unreadable,
analysis_version = EXCLUDED.analysis_version,
analyzed_at = now();
-- name: DeleteTrackLoudness :exec
-- The scan saw new bytes at this path. The stored measurement describes the old
-- ones, so it goes, and the backfill measures the file again.
DELETE FROM track_loudness WHERE track_id = $1;
-- name: ListTracksNeedingLoudness :many
-- The backfill's work queue: tracks with no measurement, or one taken by an
-- older method. Keyset-paged on id so a pass visits each track at most once;
-- an inconclusive attempt writes no row, and without the cursor a file that
-- keeps timing out would be listed again straight away. Missing tracks are
-- skipped: there is no file to read.
SELECT t.id, t.file_path, t.duration_ms
FROM tracks t
LEFT JOIN track_loudness l ON l.track_id = t.id
WHERE t.missing_since IS NULL
AND (l.track_id IS NULL OR l.analysis_version < sqlc.arg(current_version))
AND t.id > sqlc.arg(after_id)
ORDER BY t.id
LIMIT sqlc.arg(batch_limit);
-- name: GetLoudnessCoverage :one
-- The admin gauge. measured + silent + unreadable + pending = total. Missing
-- tracks are excluded, or the gauge could never reach the end.
SELECT count(*)::bigint AS total,
count(*) FILTER (
WHERE l.analysis_version >= sqlc.arg(current_version)
AND l.integrated_lufs IS NOT NULL
)::bigint AS measured,
count(*) FILTER (
WHERE l.analysis_version >= sqlc.arg(current_version)
AND l.integrated_lufs IS NULL AND NOT l.unreadable
)::bigint AS silent,
count(*) FILTER (
WHERE l.analysis_version >= sqlc.arg(current_version)
AND l.unreadable
)::bigint AS unreadable,
count(*) FILTER (
WHERE l.track_id IS NULL OR l.analysis_version < sqlc.arg(current_version)
)::bigint AS pending
FROM tracks t
LEFT JOIN track_loudness l ON l.track_id = t.id
WHERE t.missing_since IS NULL;
-- name: GetLoudnessSettings :one
SELECT * FROM loudness_settings WHERE id = true;
-- name: UpdateLoudnessSettings :one
-- Whole-row write from the admin card; migration 0065's CHECK is the backstop
-- behind the service's own validation.
UPDATE loudness_settings
SET enabled = sqlc.arg(enabled),
backfill_concurrency = sqlc.arg(backfill_concurrency),
updated_at = now()
WHERE id = true
RETURNING *;
-- name: ListAlbumsNeedingLoudness :many
-- The album pass's work queue (#4996): albums whose present tracks or their
-- measurements have changed since album loudness was last computed, or that
-- never had it. The digest is over every present track's id and the
-- measurement it holds at the current version ('-' for none), so a track
-- joining, leaving or being re-measured changes it. Keyset-paged on album id
-- so a pass ends even if storing one album keeps failing.
WITH present AS (
SELECT t.album_id,
md5(string_agg(
t.id::text || ':' || coalesce(
l.analysis_version::text || '@' || l.analyzed_at::text, '-'),
',' ORDER BY t.id)) AS digest
FROM tracks t
LEFT JOIN track_loudness l
ON l.track_id = t.id AND l.analysis_version >= sqlc.arg(current_version)::smallint
WHERE t.missing_since IS NULL
GROUP BY t.album_id
)
SELECT c.album_id, c.digest::text AS digest
FROM present c
LEFT JOIN album_loudness a ON a.album_id = c.album_id
WHERE (a.album_id IS NULL OR a.inputs_digest <> c.digest)
-- Casts: sqlc cannot infer a parameter's type through a CTE alias.
AND c.album_id > sqlc.arg(after_id)::uuid
ORDER BY c.album_id
LIMIT sqlc.arg(batch_limit)::integer;
-- name: ListAlbumLoudnessInputs :many
-- Every present track on one album with its current measurement, if any.
-- settled is false for a track not yet measured at the current version.
SELECT t.id,
(l.track_id IS NOT NULL)::boolean AS settled,
l.true_peak_dbtp,
l.block_hist_start,
l.block_hist
FROM tracks t
LEFT JOIN track_loudness l
ON l.track_id = t.id AND l.analysis_version >= sqlc.arg(current_version)
WHERE t.album_id = sqlc.arg(album_id)
AND t.missing_since IS NULL;
-- name: UpsertAlbumLoudness :exec
INSERT INTO album_loudness (
album_id, integrated_lufs, true_peak_dbtp, tracks_total, tracks_settled, inputs_digest
) VALUES (
sqlc.arg(album_id), sqlc.narg(integrated_lufs), sqlc.narg(true_peak_dbtp),
sqlc.arg(tracks_total), sqlc.arg(tracks_settled), sqlc.arg(inputs_digest)
)
ON CONFLICT (album_id) DO UPDATE SET
integrated_lufs = EXCLUDED.integrated_lufs,
true_peak_dbtp = EXCLUDED.true_peak_dbtp,
tracks_total = EXCLUDED.tracks_total,
tracks_settled = EXCLUDED.tracks_settled,
inputs_digest = EXCLUDED.inputs_digest,
computed_at = now();
-- name: ListOrphanAlbumLoudness :many
-- Album rows whose album has no present track left (every file missing). The
-- album row itself survives a missing file, so its loudness would otherwise
-- stay behind describing tracks that are gone; it is recomputed if they return.
SELECT a.album_id
FROM album_loudness a
WHERE NOT EXISTS (
SELECT 1 FROM tracks t WHERE t.album_id = a.album_id AND t.missing_since IS NULL
);
-- name: DeleteAlbumLoudness :exec
DELETE FROM album_loudness WHERE album_id = ANY(sqlc.arg(album_ids)::uuid[]);
-- name: GetAlbumLoudness :one
-- The stored album values, read before a recompute so a sync change is
-- logged only when they actually move.
SELECT integrated_lufs, true_peak_dbtp FROM album_loudness WHERE album_id = $1;
-- name: GetReplayGainByTrackIDs :many
-- Track and album loudness for a set of tracks, for every surface that hands
-- gains to a client (#4997). A measurement from an older analysis version is
-- still delivered: it is a better gain than none until the backfill redoes it.
SELECT t.id,
tl.integrated_lufs AS track_lufs,
tl.true_peak_dbtp AS track_peak_dbtp,
al.integrated_lufs AS album_lufs,
al.true_peak_dbtp AS album_peak_dbtp
FROM tracks t
LEFT JOIN track_loudness tl ON tl.track_id = t.id
LEFT JOIN album_loudness al ON al.album_id = t.album_id
WHERE t.id = ANY(sqlc.arg(ids)::uuid[]);
-- name: GetAlbumLoudnessByIDs :many
SELECT album_id, integrated_lufs, true_peak_dbtp
FROM album_loudness
WHERE album_id = ANY(sqlc.arg(ids)::uuid[]);