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}) }