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>
153 lines
5.4 KiB
Go
153 lines
5.4 KiB
Go
package api
|
|
|
|
// Wire shapes for /api/library/sync upserts. The sqlc-generated row
|
|
// structs (dbq.Artist, dbq.Album, dbq.Track, dbq.Playlist) have no
|
|
// JSON tags (sqlc.yaml: emit_json_tags=false), so a raw json.Marshal
|
|
// of them produces PascalCase field names that the Flutter sync
|
|
// controller (which reads snake_case keys) can't parse.
|
|
//
|
|
// These view structs add the JSON tag layer + flatten pgtype.UUID and
|
|
// pgtype.Timestamptz / Date into strings, mirroring the pattern
|
|
// playlistRowView already established for /api/playlists.
|
|
//
|
|
// Field names match what the Android client deserialises in
|
|
// models/wire/SyncResponseWire.kt (Sync{Artist,Album,Track}Wire) exactly.
|
|
// Adding a field server-side requires a matching @SerialName there or it is
|
|
// silently dropped — kotlinx.serialization ignores unknown keys, so the
|
|
// failure is a missing value at runtime, not an error at parse time.
|
|
|
|
import (
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
|
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
|
|
)
|
|
|
|
type artistSyncView struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
SortName string `json:"sort_name"`
|
|
Mbid *string `json:"mbid"`
|
|
ArtistThumbPath *string `json:"artist_thumb_path"`
|
|
ArtistFanartPath *string `json:"artist_fanart_path"`
|
|
}
|
|
|
|
func toArtistSyncView(a dbq.Artist) artistSyncView {
|
|
return artistSyncView{
|
|
ID: syncpkg.FormatUUID(a.ID),
|
|
Name: a.Name,
|
|
SortName: a.SortName,
|
|
Mbid: a.Mbid,
|
|
ArtistThumbPath: a.ArtistThumbPath,
|
|
ArtistFanartPath: a.ArtistFanartPath,
|
|
}
|
|
}
|
|
|
|
type albumSyncView struct {
|
|
ID string `json:"id"`
|
|
ArtistID string `json:"artist_id"`
|
|
Title string `json:"title"`
|
|
SortTitle string `json:"sort_title"`
|
|
ReleaseDate *string `json:"release_date"`
|
|
CoverArtPath *string `json:"cover_art_path"`
|
|
Mbid *string `json:"mbid"`
|
|
// AlbumGain and AlbumPeak are the album's ReplayGain 2.0 values (#4997):
|
|
// dB to the -18 LUFS reference, and a linear peak. Null until every track
|
|
// on the album is measured.
|
|
AlbumGain *float32 `json:"album_gain"`
|
|
AlbumPeak *float32 `json:"album_peak"`
|
|
}
|
|
|
|
func toAlbumSyncView(a dbq.Album, g library.ReplayGain) albumSyncView {
|
|
var releaseDate *string
|
|
if a.ReleaseDate.Valid {
|
|
s := a.ReleaseDate.Time.Format("2006-01-02")
|
|
releaseDate = &s
|
|
}
|
|
return albumSyncView{
|
|
ID: syncpkg.FormatUUID(a.ID),
|
|
ArtistID: syncpkg.FormatUUID(a.ArtistID),
|
|
Title: a.Title,
|
|
SortTitle: a.SortTitle,
|
|
ReleaseDate: releaseDate,
|
|
CoverArtPath: a.CoverArtPath,
|
|
Mbid: a.Mbid,
|
|
AlbumGain: g.AlbumGain,
|
|
AlbumPeak: g.AlbumPeak,
|
|
}
|
|
}
|
|
|
|
type trackSyncView struct {
|
|
ID string `json:"id"`
|
|
AlbumID string `json:"album_id"`
|
|
ArtistID string `json:"artist_id"`
|
|
Title string `json:"title"`
|
|
DurationMs int32 `json:"duration_ms"`
|
|
TrackNumber *int32 `json:"track_number"`
|
|
DiscNumber *int32 `json:"disc_number"`
|
|
FilePath string `json:"file_path"`
|
|
FileFormat string `json:"file_format"`
|
|
Genre *string `json:"genre"`
|
|
// Missing reports that the file is currently absent from disk (#2704).
|
|
//
|
|
// Shipped as state rather than filtered out of the feed, because a
|
|
// missing file is expected to come back: the scanner clears the mark
|
|
// when it does, and adopts the row if it returns under a new name
|
|
// (#2528). Dropping the row instead would mean a delete-and-recreate on
|
|
// every client for what is often a transient unmount, churning caches
|
|
// and discarding the identity #2528 works to preserve.
|
|
//
|
|
// A bool rather than the timestamp: clients need it to decide whether a
|
|
// track is playable, which is a yes/no. The "gone since" clock is an
|
|
// operator concern and lives on the admin surface.
|
|
Missing bool `json:"missing"`
|
|
// TrackGain and TrackPeak are the track's ReplayGain 2.0 values (#4997).
|
|
// Null until the track is measured. The album's pair rides on the album
|
|
// view, since it changes when the album does, not when this track does.
|
|
TrackGain *float32 `json:"track_gain"`
|
|
TrackPeak *float32 `json:"track_peak"`
|
|
}
|
|
|
|
func toTrackSyncView(t dbq.Track, g library.ReplayGain) trackSyncView {
|
|
return trackSyncView{
|
|
ID: syncpkg.FormatUUID(t.ID),
|
|
AlbumID: syncpkg.FormatUUID(t.AlbumID),
|
|
ArtistID: syncpkg.FormatUUID(t.ArtistID),
|
|
Title: t.Title,
|
|
DurationMs: t.DurationMs,
|
|
TrackNumber: t.TrackNumber,
|
|
DiscNumber: t.DiscNumber,
|
|
FilePath: t.FilePath,
|
|
FileFormat: t.FileFormat,
|
|
Genre: t.Genre,
|
|
Missing: t.MissingSince.Valid,
|
|
TrackGain: g.TrackGain,
|
|
TrackPeak: g.TrackPeak,
|
|
}
|
|
}
|
|
|
|
type playlistSyncView struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
IsPublic bool `json:"is_public"`
|
|
CoverPath *string `json:"cover_path"`
|
|
TrackCount int32 `json:"track_count"`
|
|
DurationSec int32 `json:"duration_sec"`
|
|
SystemVariant *string `json:"system_variant"`
|
|
}
|
|
|
|
func toPlaylistSyncView(p dbq.Playlist) playlistSyncView {
|
|
return playlistSyncView{
|
|
ID: syncpkg.FormatUUID(p.ID),
|
|
UserID: syncpkg.FormatUUID(p.UserID),
|
|
Name: p.Name,
|
|
Description: p.Description,
|
|
IsPublic: p.IsPublic,
|
|
CoverPath: p.CoverPath,
|
|
TrackCount: p.TrackCount,
|
|
DurationSec: p.DurationSec,
|
|
SystemVariant: p.SystemVariant,
|
|
}
|
|
}
|