138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
package allanime
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mal/internal/domain"
|
|
errlog "mal/pkg"
|
|
netutil "mal/pkg/net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
allAnimeBaseURL = "https://api.allanime.day"
|
|
allAnimeSiteURL = "https://allanime.day"
|
|
allAnimeReferer = "https://youtu-chan.com"
|
|
allAnimeOrigin = "https://youtu-chan.com"
|
|
defaultUserAgent = netutil.Firefox121
|
|
)
|
|
|
|
type AllAnimeProvider struct {
|
|
httpClient *http.Client
|
|
utlsClient *http.Client
|
|
extractor *providerExtractor
|
|
}
|
|
|
|
func NewAllAnimeProvider() *AllAnimeProvider {
|
|
return &AllAnimeProvider{
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
utlsClient: &http.Client{
|
|
Transport: &netutil.UtlsRoundTripper{},
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
extractor: newProviderExtractor(),
|
|
}
|
|
}
|
|
|
|
func (c *AllAnimeProvider) Name() string {
|
|
return "AllAnime"
|
|
}
|
|
|
|
func (c *AllAnimeProvider) GetStreams(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string) (*domain.StreamResult, error) {
|
|
showID := c.showID(ctx, animeID, titleCandidates, mode)
|
|
if showID == "" {
|
|
return nil, fmt.Errorf("allanime: show not found for malID %d", animeID)
|
|
}
|
|
|
|
sources, err := c.GetEpisodeSources(ctx, showID, episode, mode)
|
|
if err != nil || len(sources) == 0 {
|
|
return nil, fmt.Errorf("allanime: no sources for show %s", showID)
|
|
}
|
|
|
|
primary := sources[0]
|
|
|
|
result := &domain.StreamResult{
|
|
URL: primary.URL,
|
|
Referer: primary.Referer,
|
|
Type: primary.Type,
|
|
}
|
|
|
|
for _, sub := range primary.Subtitles {
|
|
result.Subtitles = append(result.Subtitles, domain.Subtitle{
|
|
Label: sub.Lang,
|
|
URL: sub.URL,
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *AllAnimeProvider) graphqlRequest(ctx context.Context, query string, variables map[string]any) (map[string]any, error) {
|
|
if mode, ok := variables["translationType"].(string); ok {
|
|
variables["translationType"] = strings.ToLower(mode)
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"query": query,
|
|
"variables": variables,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal graphql payload: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, allAnimeBaseURL+"/api", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create graphql request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Referer", allAnimeReferer)
|
|
req.Header.Set("User-Agent", defaultUserAgent)
|
|
|
|
statusCode, respBody, err := executeAndReadResponse(c.httpClient, req, "execute graphql request", "read graphql response")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if statusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("graphql status %d", statusCode)
|
|
}
|
|
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
|
return nil, fmt.Errorf("decode graphql response: %w", err)
|
|
}
|
|
|
|
if errs, ok := parsed["errors"].([]any); ok && len(errs) > 0 {
|
|
return nil, fmt.Errorf("graphql error: %v", errs[0])
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func executeAndReadResponse(client *http.Client, req *http.Request, executeErrPrefix string, readErrPrefix string) (int, []byte, error) {
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("%s: %w", executeErrPrefix, err)
|
|
}
|
|
defer func() {
|
|
errlog.Log("failed to close allanime response body", resp.Body.Close())
|
|
}()
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, netutil.MiB2))
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("%s: %w", readErrPrefix, err)
|
|
}
|
|
|
|
return resp.StatusCode, body, nil
|
|
}
|