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>
499 lines
14 KiB
Go
499 lines
14 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: loudness.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const deleteAlbumLoudness = `-- name: DeleteAlbumLoudness :exec
|
|
DELETE FROM album_loudness WHERE album_id = ANY($1::uuid[])
|
|
`
|
|
|
|
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
|
|
DELETE FROM track_loudness WHERE track_id = $1
|
|
`
|
|
|
|
// 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.
|
|
func (q *Queries) DeleteTrackLoudness(ctx context.Context, trackID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteTrackLoudness, trackID)
|
|
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 (
|
|
WHERE l.analysis_version >= $1
|
|
AND l.integrated_lufs IS NOT NULL
|
|
)::bigint AS measured,
|
|
count(*) FILTER (
|
|
WHERE l.analysis_version >= $1
|
|
AND l.integrated_lufs IS NULL AND NOT l.unreadable
|
|
)::bigint AS silent,
|
|
count(*) FILTER (
|
|
WHERE l.analysis_version >= $1
|
|
AND l.unreadable
|
|
)::bigint AS unreadable,
|
|
count(*) FILTER (
|
|
WHERE l.track_id IS NULL OR l.analysis_version < $1
|
|
)::bigint AS pending
|
|
FROM tracks t
|
|
LEFT JOIN track_loudness l ON l.track_id = t.id
|
|
WHERE t.missing_since IS NULL
|
|
`
|
|
|
|
type GetLoudnessCoverageRow struct {
|
|
Total int64
|
|
Measured int64
|
|
Silent int64
|
|
Unreadable int64
|
|
Pending int64
|
|
}
|
|
|
|
// The admin gauge. measured + silent + unreadable + pending = total. Missing
|
|
// tracks are excluded, or the gauge could never reach the end.
|
|
func (q *Queries) GetLoudnessCoverage(ctx context.Context, currentVersion int16) (GetLoudnessCoverageRow, error) {
|
|
row := q.db.QueryRow(ctx, getLoudnessCoverage, currentVersion)
|
|
var i GetLoudnessCoverageRow
|
|
err := row.Scan(
|
|
&i.Total,
|
|
&i.Measured,
|
|
&i.Silent,
|
|
&i.Unreadable,
|
|
&i.Pending,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getLoudnessSettings = `-- name: GetLoudnessSettings :one
|
|
SELECT id, enabled, backfill_concurrency, updated_at FROM loudness_settings WHERE id = true
|
|
`
|
|
|
|
func (q *Queries) GetLoudnessSettings(ctx context.Context) (LoudnessSetting, error) {
|
|
row := q.db.QueryRow(ctx, getLoudnessSettings)
|
|
var i LoudnessSetting
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Enabled,
|
|
&i.BackfillConcurrency,
|
|
&i.UpdatedAt,
|
|
)
|
|
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,
|
|
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 >= $1
|
|
WHERE t.album_id = $2
|
|
AND t.missing_since IS NULL
|
|
`
|
|
|
|
type ListAlbumLoudnessInputsParams struct {
|
|
CurrentVersion int16
|
|
AlbumID pgtype.UUID
|
|
}
|
|
|
|
type ListAlbumLoudnessInputsRow struct {
|
|
ID pgtype.UUID
|
|
Settled bool
|
|
TruePeakDbtp *float32
|
|
BlockHistStart *int16
|
|
BlockHist []int32
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListAlbumLoudnessInputs(ctx context.Context, arg ListAlbumLoudnessInputsParams) ([]ListAlbumLoudnessInputsRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumLoudnessInputs, arg.CurrentVersion, arg.AlbumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumLoudnessInputsRow
|
|
for rows.Next() {
|
|
var i ListAlbumLoudnessInputsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Settled,
|
|
&i.TruePeakDbtp,
|
|
&i.BlockHistStart,
|
|
&i.BlockHist,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsNeedingLoudness = `-- name: ListAlbumsNeedingLoudness :many
|
|
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 >= $3::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 > $1::uuid
|
|
ORDER BY c.album_id
|
|
LIMIT $2::integer
|
|
`
|
|
|
|
type ListAlbumsNeedingLoudnessParams struct {
|
|
AfterID pgtype.UUID
|
|
BatchLimit int32
|
|
CurrentVersion int16
|
|
}
|
|
|
|
type ListAlbumsNeedingLoudnessRow struct {
|
|
AlbumID pgtype.UUID
|
|
Digest string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListAlbumsNeedingLoudness(ctx context.Context, arg ListAlbumsNeedingLoudnessParams) ([]ListAlbumsNeedingLoudnessRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsNeedingLoudness, arg.AfterID, arg.BatchLimit, arg.CurrentVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsNeedingLoudnessRow
|
|
for rows.Next() {
|
|
var i ListAlbumsNeedingLoudnessRow
|
|
if err := rows.Scan(&i.AlbumID, &i.Digest); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
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 < $1)
|
|
AND t.id > $2
|
|
ORDER BY t.id
|
|
LIMIT $3
|
|
`
|
|
|
|
type ListTracksNeedingLoudnessParams struct {
|
|
CurrentVersion int16
|
|
AfterID pgtype.UUID
|
|
BatchLimit int32
|
|
}
|
|
|
|
type ListTracksNeedingLoudnessRow struct {
|
|
ID pgtype.UUID
|
|
FilePath string
|
|
DurationMs int32
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListTracksNeedingLoudness(ctx context.Context, arg ListTracksNeedingLoudnessParams) ([]ListTracksNeedingLoudnessRow, error) {
|
|
rows, err := q.db.Query(ctx, listTracksNeedingLoudness, arg.CurrentVersion, arg.AfterID, arg.BatchLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListTracksNeedingLoudnessRow
|
|
for rows.Next() {
|
|
var i ListTracksNeedingLoudnessRow
|
|
if err := rows.Scan(&i.ID, &i.FilePath, &i.DurationMs); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateLoudnessSettings = `-- name: UpdateLoudnessSettings :one
|
|
UPDATE loudness_settings
|
|
SET enabled = $1,
|
|
backfill_concurrency = $2,
|
|
updated_at = now()
|
|
WHERE id = true
|
|
RETURNING id, enabled, backfill_concurrency, updated_at
|
|
`
|
|
|
|
type UpdateLoudnessSettingsParams struct {
|
|
Enabled bool
|
|
BackfillConcurrency int32
|
|
}
|
|
|
|
// Whole-row write from the admin card; migration 0065's CHECK is the backstop
|
|
// behind the service's own validation.
|
|
func (q *Queries) UpdateLoudnessSettings(ctx context.Context, arg UpdateLoudnessSettingsParams) (LoudnessSetting, error) {
|
|
row := q.db.QueryRow(ctx, updateLoudnessSettings, arg.Enabled, arg.BackfillConcurrency)
|
|
var i LoudnessSetting
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Enabled,
|
|
&i.BackfillConcurrency,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertAlbumLoudness = `-- name: UpsertAlbumLoudness :exec
|
|
INSERT INTO album_loudness (
|
|
album_id, integrated_lufs, true_peak_dbtp, tracks_total, tracks_settled, inputs_digest
|
|
) VALUES (
|
|
$1, $2, $3,
|
|
$4, $5, $6
|
|
)
|
|
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()
|
|
`
|
|
|
|
type UpsertAlbumLoudnessParams struct {
|
|
AlbumID pgtype.UUID
|
|
IntegratedLufs *float32
|
|
TruePeakDbtp *float32
|
|
TracksTotal int32
|
|
TracksSettled int32
|
|
InputsDigest string
|
|
}
|
|
|
|
func (q *Queries) UpsertAlbumLoudness(ctx context.Context, arg UpsertAlbumLoudnessParams) error {
|
|
_, err := q.db.Exec(ctx, upsertAlbumLoudness,
|
|
arg.AlbumID,
|
|
arg.IntegratedLufs,
|
|
arg.TruePeakDbtp,
|
|
arg.TracksTotal,
|
|
arg.TracksSettled,
|
|
arg.InputsDigest,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const upsertTrackLoudness = `-- name: UpsertTrackLoudness :exec
|
|
INSERT INTO track_loudness (
|
|
track_id, integrated_lufs, true_peak_dbtp, loudness_range_lu,
|
|
block_hist_start, block_hist, unreadable, analysis_version
|
|
) VALUES (
|
|
$1, $2, $3,
|
|
$4, $5, $6,
|
|
$7, $8
|
|
)
|
|
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()
|
|
`
|
|
|
|
type UpsertTrackLoudnessParams struct {
|
|
TrackID pgtype.UUID
|
|
IntegratedLufs *float32
|
|
TruePeakDbtp *float32
|
|
LoudnessRangeLu *float32
|
|
BlockHistStart *int16
|
|
BlockHist []int32
|
|
Unreadable bool
|
|
AnalysisVersion int16
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) UpsertTrackLoudness(ctx context.Context, arg UpsertTrackLoudnessParams) error {
|
|
_, err := q.db.Exec(ctx, upsertTrackLoudness,
|
|
arg.TrackID,
|
|
arg.IntegratedLufs,
|
|
arg.TruePeakDbtp,
|
|
arg.LoudnessRangeLu,
|
|
arg.BlockHistStart,
|
|
arg.BlockHist,
|
|
arg.Unreadable,
|
|
arg.AnalysisVersion,
|
|
)
|
|
return err
|
|
}
|