diff --git a/.gitignore b/.gitignore index 8ead06c..1548383 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Content-addressed media files /data +/data0 # Test binary, built with `go test -c` *.test diff --git a/album.go b/album.go index 3d00026..be020cf 100644 --- a/album.go +++ b/album.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "net/http" "os" ) @@ -108,13 +109,16 @@ func (a *Album) Enrich(ctx context.Context, n Net) error { } var v AlbumResponse - if body, _, err := n.Get(ctx, APIPrefix+"/album/"+a.CID.String()+"/detail"); err != nil { + 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(body).Decode(&v); err != nil { - return errors.Join(body.Close(), err) + if err = json.NewDecoder(resp.Body).Decode(&v); err != nil { + return errors.Join(resp.Body.Close(), err) } - if err = body.Close(); err != nil { + if err = resp.Body.Close(); err != nil { return err } } diff --git a/album_test.go b/album_test.go index 68fee2d..509fadf 100644 --- a/album_test.go +++ b/album_test.go @@ -183,6 +183,19 @@ func TestAlbumEnrich(t *testing.T) { makeAlbumPath(1030, true): []byte{0}, }, Album{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))}, + {"status", 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): []byte{0xde, 0xad, 0xbe, 0xef}, + }, Album{}, &ResponseStatusError{ + URL: "https://monster-siren.hypergryph.com/api/album/1030/detail", + StatusCode: 0xdeadbeef, + CloseErr: os.ErrInvalid, + }}, + {"close", Album{ CID: 1030, Name: "Speed of Light", diff --git a/cmd/msrfetch/fetch.go b/cmd/msrfetch/fetch.go index d8fce25..0619ed3 100644 --- a/cmd/msrfetch/fetch.go +++ b/cmd/msrfetch/fetch.go @@ -8,6 +8,7 @@ import ( "flag" "io" "log" + "net/http" "os" "path" "slices" @@ -97,19 +98,21 @@ func mustFetch(ctx context.Context) { defer wg.Done() for u := range uc { buf := new(bytes.Buffer) - if r, l, err := n.Get(ctx, u); err != nil { + if resp, err := n.Get(ctx, u); err != nil { log.Fatal(err) + } else if resp.StatusCode != http.StatusOK { + log.Fatal(&monstersirenfetch.ResponseStatusError{URL: u, StatusCode: resp.StatusCode, CloseErr: resp.Body.Close()}) } else { - if v := int(l); v > 0 { + if v := int(resp.ContentLength); v > 0 { buf.Grow(v) } - if _, err = io.Copy(buf, r); err != nil { - if closeErr := r.Close(); closeErr != nil { + if _, err = io.Copy(buf, resp.Body); err != nil { + if closeErr := resp.Body.Close(); closeErr != nil { log.Print(closeErr) } log.Fatal(err) } - if err = r.Close(); err != nil { + if err = resp.Body.Close(); err != nil { log.Fatal(err) } } diff --git a/cmd/msrfetch/net.go b/cmd/msrfetch/net.go index 805f22c..5c05171 100644 --- a/cmd/msrfetch/net.go +++ b/cmd/msrfetch/net.go @@ -2,7 +2,6 @@ package main import ( "context" - "io" "net/http" ) @@ -10,12 +9,12 @@ type netDirect struct { c http.Client } -func (n *netDirect) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) { +func (n *netDirect) Get(ctx context.Context, url string) (*http.Response, error) { var resp *http.Response if req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil); err != nil { - return nil, -2, err + return nil, err } else if resp, err = n.c.Do(req); err != nil { - return nil, -2, err + return nil, err } - return resp.Body, resp.ContentLength, nil + return resp, nil } diff --git a/generic.go b/generic.go index 22abffc..e77ef8a 100644 --- a/generic.go +++ b/generic.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "strconv" "strings" ) @@ -30,7 +31,21 @@ func (e *InconsistentEnrichError[T]) Error() string { type Net interface { // Get makes a get request to url and returns the response body. // The caller must close the body after it finishes reading from it. - Get(ctx context.Context, url string) (body io.ReadCloser, contentLength int64, err error) + Get(ctx context.Context, url string) (resp *http.Response, err error) +} + +// ResponseStatusError is returned when MSR responds with a StatusCode other than [http.StatusOK]. +type ResponseStatusError struct { + // URL is the full uninterpreted url string for this request. + URL string + // StatusCode holds the [http.Response] field of the same name. + StatusCode int + // CloseErr holds the error returned closing the response body. + CloseErr error +} + +func (e *ResponseStatusError) Error() string { + return e.URL + " responded with status code " + strconv.Itoa(e.StatusCode) } // Metadata represents metadata, often enriched, of the entire website. diff --git a/generic_test.go b/generic_test.go index bd49a31..2e4fb47 100644 --- a/generic_test.go +++ b/generic_test.go @@ -6,9 +6,11 @@ import ( "encoding/json" "fmt" "io" + "net/http" "os" "reflect" "strconv" + "strings" "testing" "unsafe" @@ -22,6 +24,13 @@ func TestInconsistentEnrichError(t *testing.T) { } } +func TestResponseStatusError(t *testing.T) { + const want = "/proc/nonexistent responded with status code 404" + if got := (&ResponseStatusError{URL: "/proc/nonexistent", StatusCode: http.StatusNotFound, CloseErr: os.ErrInvalid}).Error(); got != want { + t.Errorf("Error: %q, want %q", got, want) + } +} + func TestNullableString(t *testing.T) { checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{ {"zero", nil, `null`, ""}, @@ -116,21 +125,25 @@ 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} +func (n stubNetErrorCloser) Get(ctx context.Context, url string) (*http.Response, error) { + resp, err := stubNet(n).Get(ctx, url) + if resp != nil && resp.Body != nil { + resp.Body = errorCloser{resp.Body} } - return r, l, err + return resp, err } type stubNet map[string][]byte -func (n stubNet) Get(_ context.Context, url string) (io.ReadCloser, int64, error) { +func (n stubNet) Get(_ context.Context, url string) (*http.Response, error) { if b, ok := n[url]; !ok { - return nil, -2, fmt.Errorf("url %s requested, but is not present", url) + return nil, fmt.Errorf("url %s requested, but is not present", url) + } else if string(b) == "\xde\xad\xbe\xef" { + return &http.Response{StatusCode: 0xdeadbeef, ContentLength: 0xdeadbeef, + Body: errorCloser{nopCloser{strings.NewReader("\xde\xad\xbe\xef")}}}, nil } else { - return nopCloser{bytes.NewReader(b)}, int64(len(b)), nil + return &http.Response{StatusCode: http.StatusOK, ContentLength: int64(len(b)), + Body: nopCloser{bytes.NewReader(b)}}, nil } } diff --git a/song.go b/song.go index 8260203..3d4e793 100644 --- a/song.go +++ b/song.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "net/http" "os" "slices" ) @@ -79,13 +80,16 @@ func (s *Song) Enrich(ctx context.Context, n Net) error { } var v SongResponse - if body, _, err := n.Get(ctx, APIPrefix+"/song/"+s.CID.String()); err != nil { + u := APIPrefix + "/song/" + s.CID.String() + 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(body).Decode(&v); err != nil { - return errors.Join(body.Close(), err) + if err = json.NewDecoder(resp.Body).Decode(&v); err != nil { + return errors.Join(resp.Body.Close(), err) } - if err = body.Close(); err != nil { + if err = resp.Body.Close(); err != nil { return err } } diff --git a/song_test.go b/song_test.go index 17d61cb..694c9ce 100644 --- a/song_test.go +++ b/song_test.go @@ -113,6 +113,19 @@ func TestSongEnrich(t *testing.T) { makeSongPath(48794): []byte{0}, }, Song{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))}, + {"status", Song{ + CID: 48794, + Name: "Warm and Small Light", + AlbumCID: 6660, + Artists: []string{"塞壬唱片-MSR"}, + }, stubNet{ + makeSongPath(48794): []byte{0xde, 0xad, 0xbe, 0xef}, + }, Song{}, &ResponseStatusError{ + URL: "https://monster-siren.hypergryph.com/api/song/048794", + StatusCode: 0xdeadbeef, + CloseErr: os.ErrInvalid, + }}, + {"close", Song{ CID: 48794, Name: "Warm and Small Light",