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
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:
+135
-14
@@ -11,22 +11,13 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const deleteOrphanAlbumLoudness = `-- name: DeleteOrphanAlbumLoudness :execrows
|
||||
DELETE 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
|
||||
)
|
||||
const deleteAlbumLoudness = `-- name: DeleteAlbumLoudness :exec
|
||||
DELETE FROM album_loudness WHERE album_id = ANY($1::uuid[])
|
||||
`
|
||||
|
||||
// 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.
|
||||
func (q *Queries) DeleteOrphanAlbumLoudness(ctx context.Context) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, deleteOrphanAlbumLoudness)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
func (q *Queries) DeleteAlbumLoudness(ctx context.Context, albumIds []pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteAlbumLoudness, albumIds)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteTrackLoudness = `-- name: DeleteTrackLoudness :exec
|
||||
@@ -40,6 +31,56 @@ func (q *Queries) DeleteTrackLoudness(ctx context.Context, trackID pgtype.UUID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAlbumLoudness = `-- name: GetAlbumLoudness :one
|
||||
SELECT integrated_lufs, true_peak_dbtp FROM album_loudness WHERE album_id = $1
|
||||
`
|
||||
|
||||
type GetAlbumLoudnessRow struct {
|
||||
IntegratedLufs *float32
|
||||
TruePeakDbtp *float32
|
||||
}
|
||||
|
||||
// The stored album values, read before a recompute so a sync change is
|
||||
// logged only when they actually move.
|
||||
func (q *Queries) GetAlbumLoudness(ctx context.Context, albumID pgtype.UUID) (GetAlbumLoudnessRow, error) {
|
||||
row := q.db.QueryRow(ctx, getAlbumLoudness, albumID)
|
||||
var i GetAlbumLoudnessRow
|
||||
err := row.Scan(&i.IntegratedLufs, &i.TruePeakDbtp)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAlbumLoudnessByIDs = `-- name: GetAlbumLoudnessByIDs :many
|
||||
SELECT album_id, integrated_lufs, true_peak_dbtp
|
||||
FROM album_loudness
|
||||
WHERE album_id = ANY($1::uuid[])
|
||||
`
|
||||
|
||||
type GetAlbumLoudnessByIDsRow struct {
|
||||
AlbumID pgtype.UUID
|
||||
IntegratedLufs *float32
|
||||
TruePeakDbtp *float32
|
||||
}
|
||||
|
||||
func (q *Queries) GetAlbumLoudnessByIDs(ctx context.Context, ids []pgtype.UUID) ([]GetAlbumLoudnessByIDsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getAlbumLoudnessByIDs, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAlbumLoudnessByIDsRow
|
||||
for rows.Next() {
|
||||
var i GetAlbumLoudnessByIDsRow
|
||||
if err := rows.Scan(&i.AlbumID, &i.IntegratedLufs, &i.TruePeakDbtp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLoudnessCoverage = `-- name: GetLoudnessCoverage :one
|
||||
SELECT count(*)::bigint AS total,
|
||||
count(*) FILTER (
|
||||
@@ -101,6 +142,55 @@ func (q *Queries) GetLoudnessSettings(ctx context.Context) (LoudnessSetting, err
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getReplayGainByTrackIDs = `-- name: GetReplayGainByTrackIDs :many
|
||||
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($1::uuid[])
|
||||
`
|
||||
|
||||
type GetReplayGainByTrackIDsRow struct {
|
||||
ID pgtype.UUID
|
||||
TrackLufs *float32
|
||||
TrackPeakDbtp *float32
|
||||
AlbumLufs *float32
|
||||
AlbumPeakDbtp *float32
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (q *Queries) GetReplayGainByTrackIDs(ctx context.Context, ids []pgtype.UUID) ([]GetReplayGainByTrackIDsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getReplayGainByTrackIDs, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetReplayGainByTrackIDsRow
|
||||
for rows.Next() {
|
||||
var i GetReplayGainByTrackIDsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TrackLufs,
|
||||
&i.TrackPeakDbtp,
|
||||
&i.AlbumLufs,
|
||||
&i.AlbumPeakDbtp,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAlbumLoudnessInputs = `-- name: ListAlbumLoudnessInputs :many
|
||||
SELECT t.id,
|
||||
(l.track_id IS NOT NULL)::boolean AS settled,
|
||||
@@ -215,6 +305,37 @@ func (q *Queries) ListAlbumsNeedingLoudness(ctx context.Context, arg ListAlbumsN
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listOrphanAlbumLoudness = `-- name: ListOrphanAlbumLoudness :many
|
||||
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
|
||||
)
|
||||
`
|
||||
|
||||
// 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.
|
||||
func (q *Queries) ListOrphanAlbumLoudness(ctx context.Context) ([]pgtype.UUID, error) {
|
||||
rows, err := q.db.Query(ctx, listOrphanAlbumLoudness)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []pgtype.UUID
|
||||
for rows.Next() {
|
||||
var album_id pgtype.UUID
|
||||
if err := rows.Scan(&album_id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, album_id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listTracksNeedingLoudness = `-- name: ListTracksNeedingLoudness :many
|
||||
SELECT t.id, t.file_path, t.duration_ms
|
||||
FROM tracks t
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
Reference in New Issue
Block a user