85d2ea6143
- Data types (SManga, SChapter, Page, MangasPage, all Filter variants) - Source interfaces (Source, CatalogueSource) with MD5-based ID generation matching Tachiyomi/Suwayomi - HTTP client with per-host rate limiting, cookie jar, and 429 retry - FlareSolverr v1 client (FLARESOLVERR_URL env) - Generic GraphQL POST helper - goquery HTML parser wrappers - Source registry (panics on duplicate ID) - Multi-stage Dockerfile (golang:1.26-alpine + distroless) and compose.yml (postgres, flaresolverr, app)
56 lines
1.2 KiB
Go
56 lines
1.2 KiB
Go
package httpclient
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
type GraphQLRequest struct {
|
|
Query string `json:"query"`
|
|
Variables any `json:"variables,omitempty"`
|
|
}
|
|
|
|
type graphQLResponse[T any] struct {
|
|
Data T `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
|
|
// Post sends a GraphQL request and unmarshals the `data` field into T.
|
|
func Post[T any](ctx context.Context, client *http.Client, url string, req GraphQLRequest, headers map[string]string) (T, error) {
|
|
var zero T
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("Accept", "application/json")
|
|
for k, v := range headers {
|
|
httpReq.Header.Set(k, v)
|
|
}
|
|
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var gqlResp graphQLResponse[T]
|
|
if err := json.NewDecoder(resp.Body).Decode(&gqlResp); err != nil {
|
|
return zero, err
|
|
}
|
|
if len(gqlResp.Errors) > 0 {
|
|
return zero, fmt.Errorf("graphql: %s", gqlResp.Errors[0].Message)
|
|
}
|
|
return gqlResp.Data, nil
|
|
}
|