Files
minstrel/internal/api/convert.go
T

118 lines
3.4 KiB
Go

package api
import (
"github.com/jackc/pgx/v5/pgtype"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// uuidToString renders a pgtype.UUID as a canonical hyphenated string.
// Returns "" when the UUID is invalid (unset). Duplicated from the subsonic
// package intentionally — the two packages must not depend on each other.
func uuidToString(u pgtype.UUID) string {
if !u.Valid {
return ""
}
b := u.Bytes
const hex = "0123456789abcdef"
out := make([]byte, 36)
j := 0
for i, x := range b {
if i == 4 || i == 6 || i == 8 || i == 10 {
out[j] = '-'
j++
}
out[j] = hex[x>>4]
out[j+1] = hex[x&0x0f]
j += 2
}
return string(out)
}
// parseUUID accepts the canonical hyphenated form produced by uuidToString.
// Returns ok=false on any malformed input so handlers can 400 cleanly.
func parseUUID(s string) (pgtype.UUID, bool) {
var u pgtype.UUID
if err := u.Scan(s); err != nil {
return pgtype.UUID{}, false
}
return u, u.Valid
}
// yearFromDate returns the 4-digit year from a pgtype.Date, or 0 when the
// date is unset. Album release years are optional at scan time.
func yearFromDate(d pgtype.Date) int {
if !d.Valid {
return 0
}
return d.Time.Year()
}
// durationMsToSec converts millisecond durations (as stored in tracks.duration_ms)
// to seconds, rounding to nearest. Callers display seconds; sub-second precision
// is not useful in UI.
func durationMsToSec(ms int32) int {
return int((ms + 500) / 1000)
}
// coverURL returns the /api relative URL for an album's cover art. The
// endpoint ships in Plan 3; until then it 404s, and the SPA does not wire
// <img src> to it.
func coverURL(albumID pgtype.UUID) string {
return "/api/albums/" + uuidToString(albumID) + "/cover"
}
// streamURL returns the /api relative URL for a track's audio stream. The
// endpoint ships in Plan 3.
func streamURL(trackID pgtype.UUID) string {
return "/api/tracks/" + uuidToString(trackID) + "/stream"
}
// artistRefFrom projects a dbq.Artist into an ArtistRef. albumCount must be
// pre-computed by the caller (one query per artist in lists).
func artistRefFrom(a dbq.Artist, albumCount int) ArtistRef {
return ArtistRef{
ID: uuidToString(a.ID),
Name: a.Name,
AlbumCount: albumCount,
}
}
// albumRefFrom projects a dbq.Album into an AlbumRef. artistName must be
// pre-resolved because Album only carries artist_id. trackCount and
// durationSec may be 0 when the caller does not compute them.
func albumRefFrom(a dbq.Album, artistName string, trackCount, durationSec int) AlbumRef {
return AlbumRef{
ID: uuidToString(a.ID),
Title: a.Title,
ArtistID: uuidToString(a.ArtistID),
ArtistName: artistName,
Year: yearFromDate(a.ReleaseDate),
TrackCount: trackCount,
DurationSec: durationSec,
CoverURL: coverURL(a.ID),
}
}
// trackRefFrom projects a dbq.Track into a TrackRef. Parent names must be
// pre-resolved because Track only carries album_id and artist_id.
func trackRefFrom(t dbq.Track, albumTitle, artistName string) TrackRef {
ref := TrackRef{
ID: uuidToString(t.ID),
Title: t.Title,
AlbumID: uuidToString(t.AlbumID),
AlbumTitle: albumTitle,
ArtistID: uuidToString(t.ArtistID),
ArtistName: artistName,
DurationSec: durationMsToSec(t.DurationMs),
StreamURL: streamURL(t.ID),
}
if t.TrackNumber != nil {
ref.TrackNumber = int(*t.TrackNumber)
}
if t.DiscNumber != nil {
ref.DiscNumber = int(*t.DiscNumber)
}
return ref
}