Yonah b5f5626b02
album: extend struct for /api/album/%d/data
This also includes tests against a sample response from the
https://monster-siren.hypergryph.com/api/album/1030/data endpoint.

Signed-off-by: Yonah <contrib@gensokyo.uk>
2025-09-18 19:29:23 +09:00

110 lines
3.3 KiB
Go

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