feat(api): deliver loudness gains to every client (M464 #4997)
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

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>
This commit is contained in:
2026-10-06 13:54:05 -04:00
co-authored by Claude Opus 5.5
parent c2f81bf8df
commit e3aa8629d3
19 changed files with 797 additions and 38 deletions
+30 -2
View File
@@ -133,11 +133,39 @@ ON CONFLICT (album_id) DO UPDATE SET
inputs_digest = EXCLUDED.inputs_digest,
computed_at = now();
-- name: DeleteOrphanAlbumLoudness :execrows
-- 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.
DELETE FROM album_loudness a
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[]);