Yonah 526a0371a4
song: zero fields for variants
Once again, helps remove code duplication and be more future-proof in
case any more variants are uncovered.

Signed-off-by: Yonah <contrib@gensokyo.uk>
2025-09-18 20:38:53 +09:00

139 lines
3.5 KiB
Go

package monstersirenfetch
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"slices"
)
// SongResponse is the response of /api/song/%d.
type SongResponse Response[Song]
// Song holds the metadata of a song.
type Song struct {
CID StringInt `json:"cid"`
Name string `json:"name"`
AlbumCID StringInt `json:"albumCid"`
SourceURL string `json:"sourceUrl,omitempty"`
LyricURL string `json:"lyricUrl,omitempty"`
MvURL string `json:"mvUrl,omitempty"`
MvCoverURL string `json:"mvCoverUrl,omitempty"`
Artists []string `json:"artists"`
}
const (
// SongVariantCurrent copies the current variant as-is.
SongVariantCurrent = iota
// SongVariantFull leaves all fields intact in the copy.
// This variant is returned by /api/song/%d.
SongVariantFull
// SongVariantBase zeroes [Song.SourceURL], [Song.LyricURL], [Song.MvURL], [Song.MvCoverURL].
// This variant is included in /api/songs.
SongVariantBase
)
// IsFull returns whether [Song] is considered to be its [SongVariantFull] variant.
func (s *Song) IsFull() bool { return s.SourceURL != "" }
// Copy makes a copy of [Song].
// For a [Song] where the IsFull method returns true, Copy zeroes fields to convert the copy into other variants.
// For [Song] where IsFull returns false, any variant other than [SongVariantCurrent] is undefined.
func (s *Song) Copy(variant int) *Song {
if s == nil {
return nil
}
v := *s
if variant == SongVariantCurrent {
return &v
}
if !s.IsFull() {
return nil
}
switch variant {
case SongVariantFull:
break
case SongVariantBase:
s.SourceURL = ""
s.LyricURL = ""
s.MvURL = ""
s.MvCoverURL = ""
default:
return nil
}
return s
}
// Enrich populates the remaining fields of a non-full [Song].
func (s *Song) Enrich(ctx context.Context, n Net) error {
if s == nil || s.IsFull() {
return os.ErrInvalid
}
var v SongResponse
if body, _, err := n.Get(ctx, APIPrefix+"/song/"+s.CID.String()); err != nil {
return err
} else {
if err = json.NewDecoder(body).Decode(&v); err != nil {
return errors.Join(body.Close(), err)
}
if err = body.Close(); err != nil {
return err
}
}
// these should be unreachable unless the server malfunctions
if s.CID != v.Data.CID {
return &InconsistentEnrichError[StringInt]{"cid", s.CID, v.Data.CID}
}
if s.Name != v.Data.Name {
return &InconsistentEnrichError[string]{"name", s.Name, v.Data.Name}
}
if s.AlbumCID != v.Data.AlbumCID {
return &InconsistentEnrichError[StringInt]{"albumCid", s.AlbumCID, v.Data.AlbumCID}
}
if !slices.Equal(s.Artists, v.Data.Artists) {
return &InconsistentEnrichError[[]string]{"artists", s.Artists, v.Data.Artists}
}
*s = v.Data
return nil
}
// songDirect is [Song] without its MarshalJSON method.
type songDirect Song
// songNullable is [Song] with corresponding nullable string fields.
type songNullable struct {
CID StringInt `json:"cid"`
Name string `json:"name"`
AlbumCID StringInt `json:"albumCid"`
SourceURL string `json:"sourceUrl"`
LyricURL NullableString `json:"lyricUrl"`
MvURL NullableString `json:"mvUrl"`
MvCoverURL NullableString `json:"mvCoverUrl"`
Artists []string `json:"artists"`
}
func (s *Song) MarshalJSON() (data []byte, err error) {
buf := new(bytes.Buffer)
e := NewEncoder(buf)
if !s.IsFull() {
err = e.Encode((*songDirect)(s))
data = buf.Bytes()
return
}
return json.Marshal(&songNullable{s.CID, s.Name, s.AlbumCID, s.SourceURL,
NullableString(s.LyricURL), NullableString(s.MvURL), NullableString(s.MvCoverURL),
s.Artists})
}