refactor: extract generic graphql client
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
|
"mal/pkg"
|
||||||
netutil "mal/pkg/net"
|
netutil "mal/pkg/net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -65,60 +66,75 @@ func (c *AllAnimeProvider) Name() string {
|
|||||||
return "AllAnime"
|
return "AllAnime"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AllAnimeProvider) Search(ctx context.Context, query string, mode string) ([]searchResult, error) {
|
const searchQuery = `query(
|
||||||
// 1. Search for the show to get its AllAnime ID
|
$search: SearchInput
|
||||||
graphqlQuery := `query($search: SearchInput, $limit: Int, $page: Int, $translationType: VaildTranslationTypeEnumType, $countryOrigin: VaildCountryOriginEnumType) {
|
$translationType: VaildTranslationTypeEnumType
|
||||||
shows(search: $search, limit: $limit, page: $page, translationType: $translationType, countryOrigin: $countryOrigin) {
|
$limit: Int = 40
|
||||||
edges {
|
$page: Int = 1
|
||||||
_id
|
$countryOrigin: VaildCountryOriginEnumType = ALL
|
||||||
malId
|
) {
|
||||||
name
|
shows(
|
||||||
}
|
search: $search
|
||||||
}
|
limit: $limit
|
||||||
}`
|
page: $page
|
||||||
|
translationType: $translationType
|
||||||
|
countryOrigin: $countryOrigin
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
_id
|
||||||
|
malId
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
variables := map[string]any{
|
func (c *AllAnimeProvider) Search(ctx context.Context, query string, mode string) ([]searchResult, error) {
|
||||||
"search": map[string]any{
|
type searchData struct {
|
||||||
"allowAdult": false,
|
Shows struct {
|
||||||
"allowUnknown": false,
|
Edges []struct {
|
||||||
"query": query,
|
ID string `json:"_id"`
|
||||||
},
|
MalID string `json:"malId"`
|
||||||
"limit": 40,
|
Name string `json:"name"`
|
||||||
"page": 1,
|
} `json:"edges"`
|
||||||
"translationType": mode,
|
} `json:"shows"`
|
||||||
"countryOrigin": "ALL",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := c.graphqlRequest(ctx, graphqlQuery, variables)
|
type searchInput struct {
|
||||||
|
AllowAdult bool `json:"allowAdult"`
|
||||||
|
AllowUnknown bool `json:"allowUnknown"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchVariables struct {
|
||||||
|
Search searchInput `json:"search"`
|
||||||
|
TranslationType string `json:"translationType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := searchVariables{
|
||||||
|
Search: searchInput{
|
||||||
|
AllowAdult: false,
|
||||||
|
AllowUnknown: false,
|
||||||
|
Query: query,
|
||||||
|
},
|
||||||
|
TranslationType: mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := graphql.Post[searchData](ctx, c.httpClient, allAnimeBaseURL+"/api", searchQuery, vars, graphql.PostOptions{
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Referer": allAnimeReferer,
|
||||||
|
"User-Agent": defaultUserAgent,
|
||||||
|
},
|
||||||
|
BodyMax: netutil.MiB2,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, ok := result["data"].(map[string]any)
|
out := make([]searchResult, 0, len(data.Shows.Edges))
|
||||||
if !ok {
|
for _, edge := range data.Shows.Edges {
|
||||||
return nil, fmt.Errorf("invalid search response")
|
id := edge.ID
|
||||||
}
|
malID := edge.MalID
|
||||||
|
name := edge.Name
|
||||||
shows, ok := data["shows"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid shows payload")
|
|
||||||
}
|
|
||||||
|
|
||||||
edges, ok := shows["edges"].([]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid search edges")
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]searchResult, 0, len(edges))
|
|
||||||
for _, edge := range edges {
|
|
||||||
item, ok := edge.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
id, _ := item["_id"].(string)
|
|
||||||
malID, _ := item["malId"].(string)
|
|
||||||
name, _ := item["name"].(string)
|
|
||||||
if unquoted, err := strconv.Unquote("\"" + name + "\""); err == nil {
|
if unquoted, err := strconv.Unquote("\"" + name + "\""); err == nil {
|
||||||
name = unquoted
|
name = unquoted
|
||||||
}
|
}
|
||||||
|
|||||||
79
pkg/graphql.go
Normal file
79
pkg/graphql.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response[T any] struct {
|
||||||
|
Data T `json:"data"`
|
||||||
|
Errors []Error `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostOptions struct {
|
||||||
|
Headers map[string]string
|
||||||
|
BodyMax int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func Post[T any](ctx context.Context, httpClient *http.Client, url string, query string, variables any, opts PostOptions) (T, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"variables": variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("graphql: marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("graphql: create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
for k, v := range opts.Headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("graphql: execute request: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
max := opts.BodyMax
|
||||||
|
if max <= 0 {
|
||||||
|
max = 2 << 20
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(io.LimitReader(resp.Body, max))
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("graphql: read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return zero, fmt.Errorf("graphql: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed Response[T]
|
||||||
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||||
|
return zero, fmt.Errorf("graphql: decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsed.Errors) > 0 {
|
||||||
|
return zero, fmt.Errorf("graphql: %s", parsed.Errors[0].Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Data, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user