package monstersirenfetch import ( "bytes" "context" "encoding/json" "errors" "net/http" "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"` } const ( // AlbumVariantCurrent copies the current variant as-is. AlbumVariantCurrent = iota // AlbumVariantFull leaves all fields intact in the copy. // This variant is not returned by any endpoint and is obtained via [Album.Enrich]. AlbumVariantFull // AlbumVariantDetail zeroes [Album.Artists]. // This variant is returned by /api/album/%s/detail. AlbumVariantDetail // AlbumVariantData zeroes [Album.Songs]. // This variant is returned by /api/album/%d/data. AlbumVariantData // AlbumVariantBase zeroes [Album.Intro], [Album.Belong], [Album.CoverDeURL], [Album.Songs]. // This variant is included in /api/albums. AlbumVariantBase ) // IsFull returns whether [Album] is considered to be its [AlbumVariantFull] variant. func (a *Album) IsFull() bool { return a.Belong != "" && len(a.Songs) > 0 } // Copy makes a copy of [Album]. // For an [Album] where the IsFull method returns true, Copy zeroes fields to convert the copy into other variants. // For [Album] where IsFull returns false, any variant other than [AlbumVariantCurrent] is undefined. func (a *Album) Copy(v *Album, variant int) bool { if v == nil || a == nil || v == a { return false } if variant == AlbumVariantCurrent { *v = *a return true } if !a.IsFull() { return false } switch variant { case AlbumVariantFull: *v = *a case AlbumVariantDetail: *v = *a v.Artists = nil case AlbumVariantData: *v = *a v.Songs = nil case AlbumVariantBase: *v = *a v.Intro = "" v.Belong = "" v.CoverDeURL = "" v.Songs = nil default: return false } return true } // 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 u := APIPrefix + "/album/" + a.CID.String() + "/detail" if resp, err := n.Get(ctx, u); err != nil { return err } else if resp.StatusCode != http.StatusOK { return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()} } else { if err = json.NewDecoder(resp.Body).Decode(&v); err != nil { return errors.Join(resp.Body.Close(), err) } if err = resp.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}) }