Files
minstrel/internal/api/library_sync_views.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

153 lines
5.3 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, and clients read snake_case
// keys.
//
// 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:
// 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.
//
// 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.
// 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 track's identity.
//
// 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
// admin concern and lives on the admin surface.
Missing bool `json:"missing"`
// TrackGain and TrackPeak are the track's ReplayGain 2.0 values.
// 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,
}
}