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:
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
)
|
||||
|
||||
// IDs on the wire are the bare UUID string. Subsonic endpoints know from
|
||||
@@ -180,6 +181,20 @@ type SongRef struct {
|
||||
BitRate int `json:"bitRate,omitempty" xml:"bitRate,attr,omitempty"`
|
||||
IsDir bool `json:"isDir" xml:"isDir,attr"`
|
||||
Type string `json:"type" xml:"type,attr"`
|
||||
// ReplayGain is OpenSubsonic's replayGain (M464 #4997): Minstrel's own
|
||||
// measurements, so third-party clients that level by ReplayGain do it
|
||||
// from the same numbers the Minstrel apps use. Omitted until measured.
|
||||
ReplayGain *ReplayGain `json:"replayGain,omitempty" xml:"replayGain,omitempty"`
|
||||
}
|
||||
|
||||
// ReplayGain is the OpenSubsonic replayGain object: gains in dB to the
|
||||
// -18 LUFS ReplayGain 2.0 reference, peaks as linear amplitude. A field with
|
||||
// no value is omitted.
|
||||
type ReplayGain struct {
|
||||
TrackGain *float32 `json:"trackGain,omitempty" xml:"trackGain,attr,omitempty"`
|
||||
AlbumGain *float32 `json:"albumGain,omitempty" xml:"albumGain,attr,omitempty"`
|
||||
TrackPeak *float32 `json:"trackPeak,omitempty" xml:"trackPeak,attr,omitempty"`
|
||||
AlbumPeak *float32 `json:"albumPeak,omitempty" xml:"albumPeak,attr,omitempty"`
|
||||
}
|
||||
|
||||
type SongResponse struct {
|
||||
@@ -284,7 +299,10 @@ func albumDetail(a dbq.Album, artistName string, songs []SongRef) AlbumDetail {
|
||||
}
|
||||
}
|
||||
|
||||
func songRef(t dbq.Track, albumTitle, artistName string) SongRef {
|
||||
// songRef takes the track's gains as an argument rather than looking them up,
|
||||
// so no response that lists songs can leave them out by forgetting a step:
|
||||
// each caller fetches them once for its whole list (replayGains).
|
||||
func songRef(t dbq.Track, albumTitle, artistName string, g library.ReplayGain) SongRef {
|
||||
s := SongRef{
|
||||
ID: uuidToID(t.ID),
|
||||
Parent: uuidToID(t.AlbumID),
|
||||
@@ -313,9 +331,30 @@ func songRef(t dbq.Track, albumTitle, artistName string) SongRef {
|
||||
if t.Genre != nil {
|
||||
s.Genre = *t.Genre
|
||||
}
|
||||
if !g.Empty() {
|
||||
s.ReplayGain = &ReplayGain{
|
||||
TrackGain: g.TrackGain, AlbumGain: g.AlbumGain,
|
||||
TrackPeak: g.TrackPeak, AlbumPeak: g.AlbumPeak,
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// replayGains fetches the gains for a list of tracks in one query. A failed
|
||||
// lookup yields no gains rather than failing the response: the songs still
|
||||
// play, just unleveled, which is how they played before gains existed.
|
||||
func replayGains(ctx context.Context, q *dbq.Queries, tracks []dbq.Track) map[pgtype.UUID]library.ReplayGain {
|
||||
ids := make([]pgtype.UUID, len(tracks))
|
||||
for i, t := range tracks {
|
||||
ids[i] = t.ID
|
||||
}
|
||||
gains, err := library.ReplayGainForTracks(ctx, q, ids)
|
||||
if err != nil {
|
||||
return map[pgtype.UUID]library.ReplayGain{}
|
||||
}
|
||||
return gains
|
||||
}
|
||||
|
||||
// coverArtID returns the album UUID as the cover-art key. getCoverArt uses
|
||||
// the album row to find art either in cover_art_path (when the scanner sets
|
||||
// it) or via sidecar lookup in the album directory, so emitting the id
|
||||
|
||||
Reference in New Issue
Block a user