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>
This commit is contained in:
parent
da4b1d86d9
commit
b5f5626b02
109
album.go
Normal file
109
album.go
Normal file
@ -0,0 +1,109 @@
|
||||
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})
|
||||
}
|
||||
172
album_test.go
Normal file
172
album_test.go
Normal file
@ -0,0 +1,172 @@
|
||||
package monstersirenfetch_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "git.gensokyo.uk/yonah/monstersirenfetch"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed testdata/album.data.json
|
||||
albumDataJSON []byte
|
||||
//go:embed testdata/album.detail.json
|
||||
albumDetailJSON []byte
|
||||
)
|
||||
|
||||
func TestAlbum(t *testing.T) {
|
||||
checkJSONRoundTrip(t, AlbumResponse{Data: Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
Intro: "在无人告知方向的黑夜里,独自以光速探寻正确的道路。 \n无论看待世界是简单,亦或是复杂,大地和天空始终让开始与结束相连。 \n《Speed of Light》,通过乐调铺陈的驰放与内敛感,希望能为您呈现超脱于现实的惬意想象空间。 \n愿您掌心的小小地图一直指引着前路。像光一样前行,如企鹅物流般永不停歇。",
|
||||
Belong: "arknights",
|
||||
CoverDeURL: "https://web.hycdn.cn/siren/pic/20210322/0bf0d84e08b57acd2455b412224ba8e8.jpg",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}}, albumDataJSON)
|
||||
}
|
||||
|
||||
func TestAlbumEnrich(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
base Album
|
||||
n Net
|
||||
want Album
|
||||
wantErr error
|
||||
}{
|
||||
{"get", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{}, Album{}, errors.New("url https://monster-siren.hypergryph.com/api/album/1030/detail requested, but is not present")},
|
||||
|
||||
{"invalid response", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(1030, true): []byte{0},
|
||||
}, Album{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
||||
|
||||
{"close", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNetErrorCloser{
|
||||
makeAlbumPath(1030, true): albumDetailJSON,
|
||||
}, Album{}, os.ErrInvalid},
|
||||
|
||||
{"inconsistent cid", Album{
|
||||
CID: 0xdeadbeef,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(0xdeadbeef, true): albumDetailJSON,
|
||||
}, Album{}, &InconsistentEnrichError[StringInt]{
|
||||
Field: "cid",
|
||||
Value: 0xdeadbeef,
|
||||
NewValue: 1030,
|
||||
}},
|
||||
|
||||
{"inconsistent name", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light\x00",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(1030, true): albumDetailJSON,
|
||||
}, Album{}, &InconsistentEnrichError[string]{
|
||||
Field: "name",
|
||||
Value: "Speed of Light\x00",
|
||||
NewValue: "Speed of Light",
|
||||
}},
|
||||
|
||||
{"inconsistent coverUrl", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg\x00",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(1030, true): albumDetailJSON,
|
||||
}, Album{}, &InconsistentEnrichError[string]{
|
||||
Field: "coverUrl",
|
||||
Value: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg\x00",
|
||||
NewValue: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
}},
|
||||
|
||||
{"unexpected artists", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(1030, true): albumDataJSON,
|
||||
}, Album{}, &InconsistentEnrichError[[]string]{
|
||||
Field: "artists",
|
||||
Value: nil,
|
||||
NewValue: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}},
|
||||
|
||||
{"valid", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNet{
|
||||
makeAlbumPath(1030, true): albumDetailJSON,
|
||||
}, Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
Intro: "在无人告知方向的黑夜里,独自以光速探寻正确的道路。 \n无论看待世界是简单,亦或是复杂,大地和天空始终让开始与结束相连。 \n《Speed of Light》,通过乐调铺陈的驰放与内敛感,希望能为您呈现超脱于现实的惬意想象空间。 \n愿您掌心的小小地图一直指引着前路。像光一样前行,如企鹅物流般永不停歇。",
|
||||
Belong: "arknights",
|
||||
CoverDeURL: "https://web.hycdn.cn/siren/pic/20210322/0bf0d84e08b57acd2455b412224ba8e8.jpg",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Songs: []AlbumSong{
|
||||
{CID: 880374, Name: "Speed of Light", Artists: []string{"塞壬唱片-MSR", "DJ Okawari"}},
|
||||
{CID: 125012, Name: "Speed of Light (Instrumental)", Artists: []string{"DJ Okawari"}},
|
||||
},
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := new(Album)
|
||||
*a = tc.base
|
||||
if err := a.Enrich(t.Context(), tc.n); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("Enrich: error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr == nil && !reflect.DeepEqual(a, &tc.want) {
|
||||
t.Errorf("Enrich: %#v, want %#v", a, &tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
if err := (*Album)(nil).Enrich(t.Context(), nil); !errors.Is(err, os.ErrInvalid) {
|
||||
t.Errorf("Enrich: error = %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("full", func(t *testing.T) {
|
||||
if err := (&Album{Belong: "\x00", Songs: []AlbumSong{{}}}).Enrich(t.Context(), nil); !errors.Is(err, os.ErrInvalid) {
|
||||
t.Errorf("Enrich: error = %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func makeAlbumPath(cid StringInt, detail bool) string {
|
||||
suffix := "data"
|
||||
if detail {
|
||||
suffix = "detail"
|
||||
}
|
||||
return APIPrefix + "/album/" + cid.String() + "/" + suffix
|
||||
}
|
||||
@ -5,11 +5,3 @@ type AlbumsResponse Response[AlbumsData]
|
||||
|
||||
// AlbumsData is the type of [AlbumsResponse] data.
|
||||
type AlbumsData []Album
|
||||
|
||||
// Album represents the metadata of an album.
|
||||
type Album struct {
|
||||
CID StringInt `json:"cid"`
|
||||
Name string `json:"name"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
Artists []string `json:"artistes"`
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.gensokyo.uk/yonah/monstersirenfetch"
|
||||
)
|
||||
@ -36,8 +37,28 @@ func mustEnrich(ctx context.Context) {
|
||||
} else {
|
||||
n := new(netDirect)
|
||||
for _, ca := range c {
|
||||
log.Printf("enriching album %s (%s)", ca.Name, ca.CID.String())
|
||||
if ca.Album == nil {
|
||||
log.Fatal("albums contains nil")
|
||||
}
|
||||
|
||||
if !ca.Album.IsFull() {
|
||||
if err = ca.Album.Enrich(ctx, n); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("enriched album %s (%s) with %d songs", ca.Album.CID.String(), ca.Album.Name, len(ca.Songs))
|
||||
} else {
|
||||
log.Printf("skipped album %s (%s)", ca.Album.CID.String(), ca.Album.Name)
|
||||
}
|
||||
|
||||
// consistency check: for later validating enriched songs against flatten
|
||||
flattenSongs := make([]monstersirenfetch.AlbumSong, 0, len(ca.Songs))
|
||||
|
||||
for _, cs := range ca.Songs {
|
||||
if cs == nil {
|
||||
log.Fatal("songs contains nil")
|
||||
}
|
||||
flattenSongs = append(flattenSongs, monstersirenfetch.AlbumSong{CID: cs.CID, Name: cs.Name, Artists: cs.Artists})
|
||||
|
||||
if !cs.IsFull() {
|
||||
if err = cs.Enrich(ctx, n); err != nil {
|
||||
log.Fatal(err)
|
||||
@ -47,6 +68,19 @@ func mustEnrich(ctx context.Context) {
|
||||
log.Printf("skipped song %s: %s (%s)", cs.CID.String(), cs.SourceURL, cs.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// consistency check: enriched songs match flattened songs
|
||||
slices.SortFunc(flattenSongs, func(a, b monstersirenfetch.AlbumSong) int { return int(a.CID - b.CID) })
|
||||
enrichSongs := make([]monstersirenfetch.AlbumSong, len(ca.Album.Songs))
|
||||
copy(enrichSongs, ca.Album.Songs)
|
||||
slices.SortFunc(enrichSongs, func(a, b monstersirenfetch.AlbumSong) int { return int(a.CID - b.CID) })
|
||||
if !slices.EqualFunc(flattenSongs, enrichSongs, func(a monstersirenfetch.AlbumSong, b monstersirenfetch.AlbumSong) bool {
|
||||
return a.CID == b.CID && a.Name == b.Name && slices.Equal(a.Artists, b.Artists)
|
||||
}) {
|
||||
log.Fatalf("album %s enrichment inconsistent with flatten state", ca.Album.Name)
|
||||
} else {
|
||||
log.Printf("validated %d songs associated with album %s", len(enrichSongs), ca.Album.CID.String())
|
||||
}
|
||||
}
|
||||
mustWriteJSON(flagOutputPath, c)
|
||||
log.Println("composite data written to", flagOutputPath)
|
||||
|
||||
@ -32,6 +32,7 @@ func mustFetch(ctx context.Context) {
|
||||
|
||||
const (
|
||||
invalidContainsNil = "invalid composite data"
|
||||
invalidNotEnriched = "this composite is not enriched"
|
||||
)
|
||||
|
||||
var urls []string
|
||||
@ -39,17 +40,23 @@ func mustFetch(ctx context.Context) {
|
||||
if ca.Album == nil {
|
||||
log.Fatal(invalidContainsNil)
|
||||
}
|
||||
if ca.CoverURL == "" {
|
||||
log.Fatalf("album %s missing coverUrl", ca.CID.String())
|
||||
if !ca.Album.IsFull() {
|
||||
log.Fatal(invalidNotEnriched)
|
||||
}
|
||||
if ca.Album.CoverURL == "" {
|
||||
log.Fatalf("album %s missing coverUrl", ca.Album.CID.String())
|
||||
}
|
||||
urls = append(urls, ca.Album.CoverURL)
|
||||
if ca.Album.CoverDeURL != "" {
|
||||
urls = append(urls, ca.Album.CoverDeURL)
|
||||
}
|
||||
urls = append(urls, ca.CoverURL)
|
||||
|
||||
for _, cs := range ca.Songs {
|
||||
if cs == nil {
|
||||
log.Fatal(invalidContainsNil)
|
||||
}
|
||||
if !cs.IsFull() {
|
||||
log.Fatal("this composite is not enriched")
|
||||
log.Fatal(invalidNotEnriched)
|
||||
}
|
||||
|
||||
urls = append(urls, cs.SourceURL)
|
||||
|
||||
11
composite.go
11
composite.go
@ -39,7 +39,8 @@ type CompositeAlbum struct {
|
||||
// Songs is a map of [Song.CID] to [Song].
|
||||
Songs SongsMap `json:"songs"`
|
||||
|
||||
*Album
|
||||
// Album is the underlying [Album].
|
||||
Album *Album `json:"data"`
|
||||
}
|
||||
|
||||
type (
|
||||
@ -56,15 +57,15 @@ func (m CompositeAlbumsMap) String() string {
|
||||
}
|
||||
|
||||
compAlbums := slices.Collect(maps.Values(m))
|
||||
slices.SortFunc(compAlbums, func(a, b CompositeAlbum) int { return int(a.CID - b.CID) })
|
||||
slices.SortFunc(compAlbums, func(a, b CompositeAlbum) int { return int(a.Album.CID - b.Album.CID) })
|
||||
|
||||
s := make([]string, len(compAlbums))
|
||||
var buf strings.Builder
|
||||
for i, ca := range compAlbums {
|
||||
buf.WriteString(
|
||||
"Album: " + ca.Name + " (" + ca.CID.String() + ")\n" +
|
||||
"Cover: " + ca.CoverURL + "\n" +
|
||||
"Artist(s): " + strings.Join(ca.Artists, ", ") + "\n" +
|
||||
"Album: " + ca.Album.Name + " (" + ca.Album.CID.String() + ")\n" +
|
||||
"Cover: " + ca.Album.CoverURL + "\n" +
|
||||
"Artist(s): " + strings.Join(ca.Album.Artists, ", ") + "\n" +
|
||||
"Songs:\n")
|
||||
|
||||
albumSongs := slices.Collect(maps.Values(ca.Songs))
|
||||
|
||||
14
generic.go
14
generic.go
@ -3,6 +3,7 @@ package monstersirenfetch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -12,6 +13,19 @@ const (
|
||||
APIPrefix = "https://monster-siren.hypergryph.com/api"
|
||||
)
|
||||
|
||||
// InconsistentEnrichError is returned by an Enrich method when a field is inconsistent
|
||||
// between base data and enrichment data.
|
||||
type InconsistentEnrichError[T any] struct {
|
||||
Field string
|
||||
Value T
|
||||
NewValue T
|
||||
}
|
||||
|
||||
func (e *InconsistentEnrichError[T]) Error() string {
|
||||
return "field " + e.Field + " inconsistent: " +
|
||||
fmt.Sprintf("%v differs from %v", e.Value, e.NewValue)
|
||||
}
|
||||
|
||||
// Net represents an abstraction over the [net] package.
|
||||
type Net interface {
|
||||
// Get makes a get request to url and returns the response body.
|
||||
|
||||
@ -2,7 +2,9 @@ package monstersirenfetch_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
@ -13,6 +15,13 @@ import (
|
||||
. "git.gensokyo.uk/yonah/monstersirenfetch"
|
||||
)
|
||||
|
||||
func TestInconsistentEnrichError(t *testing.T) {
|
||||
const want = "field cid inconsistent: 48794 differs from 3735928559"
|
||||
if got := (&InconsistentEnrichError[StringInt]{Field: "cid", Value: 48794, NewValue: 0xdeadbeef}).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullableString(t *testing.T) {
|
||||
checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
|
||||
{"zero", nil, `null`, ""},
|
||||
@ -105,6 +114,26 @@ func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
|
||||
})
|
||||
}
|
||||
|
||||
type stubNetErrorCloser stubNet
|
||||
|
||||
func (n stubNetErrorCloser) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
r, l, err := stubNet(n).Get(ctx, url)
|
||||
if r != nil {
|
||||
r = errorCloser{r}
|
||||
}
|
||||
return r, l, err
|
||||
}
|
||||
|
||||
type stubNet map[string][]byte
|
||||
|
||||
func (n stubNet) Get(_ context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
if b, ok := n[url]; !ok {
|
||||
return nil, -2, fmt.Errorf("url %s requested, but is not present", url)
|
||||
} else {
|
||||
return nopCloser{bytes.NewReader(b)}, int64(len(b)), nil
|
||||
}
|
||||
}
|
||||
|
||||
type errorCloser struct{ io.Reader }
|
||||
|
||||
func (errorCloser) Close() error { return os.ErrInvalid }
|
||||
|
||||
23
song.go
23
song.go
@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
@ -29,17 +28,6 @@ type Song struct {
|
||||
// IsFull returns whether the metadata held by [Song] is considered full (originating from a [SongResponse]).
|
||||
func (s *Song) IsFull() bool { return s.SourceURL != "" }
|
||||
|
||||
type InconsistentSongError[T any] struct {
|
||||
Field string
|
||||
Value T
|
||||
NewValue T
|
||||
}
|
||||
|
||||
func (e *InconsistentSongError[T]) Error() string {
|
||||
return "field " + e.Field + " inconsistent: " +
|
||||
fmt.Sprintf("%v differs from %v", e.Value, e.NewValue)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@ -60,16 +48,16 @@ func (s *Song) Enrich(ctx context.Context, n Net) error {
|
||||
|
||||
// these should be unreachable unless the server malfunctions
|
||||
if s.CID != v.Data.CID {
|
||||
return &InconsistentSongError[StringInt]{"cid", s.CID, v.Data.CID}
|
||||
return &InconsistentEnrichError[StringInt]{"cid", s.CID, v.Data.CID}
|
||||
}
|
||||
if s.Name != v.Data.Name {
|
||||
return &InconsistentSongError[string]{"name", s.Name, v.Data.Name}
|
||||
return &InconsistentEnrichError[string]{"name", s.Name, v.Data.Name}
|
||||
}
|
||||
if s.AlbumCID != v.Data.AlbumCID {
|
||||
return &InconsistentSongError[StringInt]{"albumCid", s.AlbumCID, v.Data.AlbumCID}
|
||||
return &InconsistentEnrichError[StringInt]{"albumCid", s.AlbumCID, v.Data.AlbumCID}
|
||||
}
|
||||
if !slices.Equal(s.Artists, v.Data.Artists) {
|
||||
return &InconsistentSongError[[]string]{"artists", s.Artists, v.Data.Artists}
|
||||
return &InconsistentEnrichError[[]string]{"artists", s.Artists, v.Data.Artists}
|
||||
}
|
||||
|
||||
*s = v.Data
|
||||
@ -93,8 +81,7 @@ type songNullable struct {
|
||||
|
||||
func (s *Song) MarshalJSON() (data []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
e := json.NewEncoder(buf)
|
||||
e.SetEscapeHTML(false)
|
||||
e := NewEncoder(buf)
|
||||
|
||||
if !s.IsFull() {
|
||||
err = e.Encode((*songDirect)(s))
|
||||
|
||||
73
song_test.go
73
song_test.go
@ -1,16 +1,10 @@
|
||||
package monstersirenfetch_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "git.gensokyo.uk/yonah/monstersirenfetch"
|
||||
@ -43,15 +37,15 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{}, Song{}, errors.New("song cid 48794 requested, but is not present")},
|
||||
}, stubNet{}, Song{}, errors.New("url https://monster-siren.hypergryph.com/api/song/048794 requested, but is not present")},
|
||||
|
||||
{"invalid response", Song{
|
||||
CID: 48794,
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{
|
||||
48794: []byte{0},
|
||||
}, stubNet{
|
||||
makeSongPath(48794): []byte{0},
|
||||
}, Song{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
||||
|
||||
{"close", Song{
|
||||
@ -59,8 +53,8 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrichErrorCloser{
|
||||
48794: songJSON,
|
||||
}, stubNetErrorCloser{
|
||||
makeSongPath(48794): songJSON,
|
||||
}, Song{}, os.ErrInvalid},
|
||||
|
||||
{"inconsistent cid", Song{
|
||||
@ -68,9 +62,9 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{
|
||||
0xdeadbeef: songJSON,
|
||||
}, Song{}, &InconsistentSongError[StringInt]{
|
||||
}, stubNet{
|
||||
makeSongPath(0xdeadbeef): songJSON,
|
||||
}, Song{}, &InconsistentEnrichError[StringInt]{
|
||||
Field: "cid",
|
||||
Value: 0xdeadbeef,
|
||||
NewValue: 48794,
|
||||
@ -81,9 +75,9 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light\x00",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{
|
||||
48794: songJSON,
|
||||
}, Song{}, &InconsistentSongError[string]{
|
||||
}, stubNet{
|
||||
makeSongPath(48794): songJSON,
|
||||
}, Song{}, &InconsistentEnrichError[string]{
|
||||
Field: "name",
|
||||
Value: "Warm and Small Light\x00",
|
||||
NewValue: "Warm and Small Light",
|
||||
@ -94,9 +88,9 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: -1,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{
|
||||
48794: songJSON,
|
||||
}, Song{}, &InconsistentSongError[StringInt]{
|
||||
}, stubNet{
|
||||
makeSongPath(48794): songJSON,
|
||||
}, Song{}, &InconsistentEnrichError[StringInt]{
|
||||
Field: "albumCid",
|
||||
Value: -1,
|
||||
NewValue: 6660,
|
||||
@ -107,9 +101,9 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR", "\x00"},
|
||||
}, stubNetSongEnrich{
|
||||
48794: songJSON,
|
||||
}, Song{}, &InconsistentSongError[[]string]{
|
||||
}, stubNet{
|
||||
makeSongPath(48794): songJSON,
|
||||
}, Song{}, &InconsistentEnrichError[[]string]{
|
||||
Field: "artists",
|
||||
Value: []string{"塞壬唱片-MSR", "\x00"},
|
||||
NewValue: []string{"塞壬唱片-MSR"},
|
||||
@ -120,8 +114,8 @@ func TestSongEnrich(t *testing.T) {
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNetSongEnrich{
|
||||
48794: songJSON,
|
||||
}, stubNet{
|
||||
makeSongPath(48794): songJSON,
|
||||
}, Song{
|
||||
CID: 48794,
|
||||
Name: "Warm and Small Light",
|
||||
@ -157,33 +151,8 @@ func TestSongEnrich(t *testing.T) {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
const want = "field cid inconsistent: 48794 differs from 3735928559"
|
||||
if got := (&InconsistentSongError[StringInt]{Field: "cid", Value: 48794, NewValue: 0xdeadbeef}).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type stubNetSongEnrichErrorCloser stubNetSongEnrich
|
||||
|
||||
func (n stubNetSongEnrichErrorCloser) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
r, l, err := stubNetSongEnrich(n).Get(ctx, url)
|
||||
if r != nil {
|
||||
r = errorCloser{r}
|
||||
}
|
||||
return r, l, err
|
||||
}
|
||||
|
||||
type stubNetSongEnrich map[StringInt][]byte
|
||||
|
||||
func (n stubNetSongEnrich) Get(_ context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
if i, err := strconv.Atoi(strings.TrimPrefix(url, APIPrefix+"/song/")); err != nil {
|
||||
return nil, -2, err
|
||||
} else if b, ok := n[StringInt(i)]; !ok {
|
||||
return nil, -2, fmt.Errorf("song cid %d requested, but is not present", i)
|
||||
} else {
|
||||
return nopCloser{bytes.NewReader(b)}, int64(len(b)), nil
|
||||
}
|
||||
func makeSongPath(cid StringInt) string {
|
||||
return APIPrefix + "/song/" + cid.String()
|
||||
}
|
||||
|
||||
1
testdata/album.data.json
vendored
Normal file
1
testdata/album.data.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"code":0,"msg":"","data":{"cid":"1030","name":"Speed of Light","intro":"在无人告知方向的黑夜里,独自以光速探寻正确的道路。 \n无论看待世界是简单,亦或是复杂,大地和天空始终让开始与结束相连。 \n《Speed of Light》,通过乐调铺陈的驰放与内敛感,希望能为您呈现超脱于现实的惬意想象空间。 \n愿您掌心的小小地图一直指引着前路。像光一样前行,如企鹅物流般永不停歇。","belong":"arknights","coverUrl":"https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg","coverDeUrl":"https://web.hycdn.cn/siren/pic/20210322/0bf0d84e08b57acd2455b412224ba8e8.jpg","artistes":["塞壬唱片-MSR","DJ Okawari"]}}
|
||||
1
testdata/album.detail.json
vendored
Normal file
1
testdata/album.detail.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"code":0,"msg":"","data":{"cid":"1030","name":"Speed of Light","intro":"在无人告知方向的黑夜里,独自以光速探寻正确的道路。 \n无论看待世界是简单,亦或是复杂,大地和天空始终让开始与结束相连。 \n《Speed of Light》,通过乐调铺陈的驰放与内敛感,希望能为您呈现超脱于现实的惬意想象空间。 \n愿您掌心的小小地图一直指引着前路。像光一样前行,如企鹅物流般永不停歇。","belong":"arknights","coverUrl":"https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg","coverDeUrl":"https://web.hycdn.cn/siren/pic/20210322/0bf0d84e08b57acd2455b412224ba8e8.jpg","songs":[{"cid":"880374","name":"Speed of Light","artistes":["塞壬唱片-MSR","DJ Okawari"]},{"cid":"125012","name":"Speed of Light (Instrumental)","artistes":["DJ Okawari"]}]}}
|
||||
2
testdata/output/composite.json
vendored
2
testdata/output/composite.json
vendored
File diff suppressed because one or more lines are too long
2258
testdata/output/composite.log
vendored
2258
testdata/output/composite.log
vendored
File diff suppressed because it is too large
Load Diff
2500
testdata/output/fetch.log
vendored
2500
testdata/output/fetch.log
vendored
File diff suppressed because it is too large
Load Diff
2
testdata/output/map.json
vendored
2
testdata/output/map.json
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user