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:
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
)
|
||||
|
||||
// maxReplayGainIDs caps one lookup. A player asks for the tracks around the
|
||||
// one it is about to play, never a whole library.
|
||||
const maxReplayGainIDs = 200
|
||||
|
||||
// replayGainResp is the wire shape for GET /api/tracks/replay-gain. Tracks
|
||||
// with no gain known are absent from items: no adjustment.
|
||||
type replayGainResp struct {
|
||||
Items map[string]library.ReplayGain `json:"items"`
|
||||
}
|
||||
|
||||
// handleGetReplayGain implements GET /api/tracks/replay-gain?ids=a,b,c (M464
|
||||
// #4997): ReplayGain 2.0 values (dB to -18 LUFS, linear peaks) for each track,
|
||||
// and for its album when the whole album is measured.
|
||||
//
|
||||
// A lookup rather than fields on TrackRef. Tracks reach the player through
|
||||
// about fifteen surfaces in two wire shapes (TrackRef and playlist entries),
|
||||
// and only the player uses the gain. One call from the player, made for the
|
||||
// tracks it is about to play, cannot be forgotten by a surface added later,
|
||||
// and costs nothing on the pages that never play anything. Android takes the
|
||||
// same values from the sync feed instead, so they are there offline.
|
||||
func (h *handlers) handleGetReplayGain(w http.ResponseWriter, r *http.Request) {
|
||||
raw := strings.TrimSpace(r.URL.Query().Get("ids"))
|
||||
if raw == "" {
|
||||
writeJSON(w, http.StatusOK, replayGainResp{Items: map[string]library.ReplayGain{}})
|
||||
return
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
if len(parts) > maxReplayGainIDs {
|
||||
writeErr(w, apierror.BadRequest("too_many_ids", "at most 200 ids per request"))
|
||||
return
|
||||
}
|
||||
ids := stringsToUUIDs(parts)
|
||||
gains, err := library.ReplayGainForTracks(r.Context(), dbq.New(h.pool), ids)
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "replay gain: lookup", apierror.InternalMsg("lookup failed", err))
|
||||
return
|
||||
}
|
||||
items := make(map[string]library.ReplayGain, len(gains))
|
||||
for id, g := range gains {
|
||||
items[uuidToString(id)] = g
|
||||
}
|
||||
writeJSON(w, http.StatusOK, replayGainResp{Items: items})
|
||||
}
|
||||
Reference in New Issue
Block a user