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>
153 lines
5.3 KiB
Go
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,
|
|
}
|
|
}
|