package monstersirenfetch import ( "bytes" "context" "encoding/json" "errors" "os" ) // AlbumResponse is the response of /api/album/%d/data and /api/album/%d/detail. type AlbumResponse Response[Album] // AlbumSong represents a minimal variant of [Song] embedded in the detail variant of [Album]. type AlbumSong struct { CID StringInt `json:"cid"` Name string `json:"name"` Artists []string `json:"artistes"` } // Album represents the metadata of an album. // Field availability is documented for each individual field. type Album struct { // CID is available for all variants of [Album]. CID StringInt `json:"cid"` // Name is available for all variants of [Album]. Name string `json:"name"` // Intro is available for data and detail variants of [Album]. Intro string `json:"intro,omitempty"` // Belong is available for data and detail variants of [Album]. Belong string `json:"belong,omitempty"` // CoverURL is available for all variants of [Album]. CoverURL string `json:"coverUrl"` // CoverDeURL is available for data and detail variants of [Album]. CoverDeURL string `json:"coverDeUrl,omitempty"` // Songs is available for the detail variant of [Album]. Songs []AlbumSong `json:"songs,omitempty"` // Artists is available for base and data variants of [Album]. Artists []string `json:"artistes"` } func (a *Album) IsFull() bool { return a.Belong != "" && len(a.Songs) > 0 } // Enrich populates the remaining fields of a non-full [Album]. func (a *Album) Enrich(ctx context.Context, n Net) error { if a == nil || a.IsFull() { return os.ErrInvalid } var v AlbumResponse if body, _, err := n.Get(ctx, APIPrefix+"/album/"+a.CID.String()+"/detail"); 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 a.CID != v.Data.CID { return &InconsistentEnrichError[StringInt]{"cid", a.CID, v.Data.CID} } if a.Name != v.Data.Name { return &InconsistentEnrichError[string]{"name", a.Name, v.Data.Name} } if a.CoverURL != v.Data.CoverURL { return &InconsistentEnrichError[string]{"coverUrl", a.CoverURL, v.Data.CoverURL} } if v.Data.Artists != nil { return &InconsistentEnrichError[[]string]{"artists", nil, v.Data.Artists} } v.Data.Artists = a.Artists *a = v.Data return nil } // albumDirect is [Album] without its MarshalJSON method. type albumDirect Album // albumNullable is [Album] with corresponding nullable string fields. type albumNullable struct { CID StringInt `json:"cid"` Name string `json:"name"` Intro NullableString `json:"intro"` Belong NullableString `json:"belong"` CoverURL string `json:"coverUrl"` CoverDeURL NullableString `json:"coverDeUrl"` Songs []AlbumSong `json:"songs,omitempty"` Artists []string `json:"artistes"` } func (a *Album) MarshalJSON() (data []byte, err error) { buf := new(bytes.Buffer) e := NewEncoder(buf) if a.Belong == "" { // this covers both data and detail variants err = e.Encode((*albumDirect)(a)) data = buf.Bytes() return } return json.Marshal(&albumNullable{a.CID, a.Name, NullableString(a.Intro), NullableString(a.Belong), a.CoverURL, NullableString(a.CoverDeURL), a.Songs, a.Artists}) }