From 557c46880e4a52cd4cb7b29a453bd79b81b9f40f Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 7 Jul 2026 20:51:35 +0200 Subject: [PATCH] refactor: consolidate http and utls clients, switch to post with aa lease --- integrations/playback/allanime/http_test.go | 71 +++++++++------------ integrations/playback/allanime/sources.go | 57 +++++++++++------ 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/integrations/playback/allanime/http_test.go b/integrations/playback/allanime/http_test.go index e58a8aca..a42c981d 100644 --- a/integrations/playback/allanime/http_test.go +++ b/integrations/playback/allanime/http_test.go @@ -150,17 +150,17 @@ func TestGraphqlRequestWithHash_Plain(t *testing.T) { t.Parallel() provider := &AllAnimeProvider{ - utlsClient: &http.Client{ + httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodGet { - t.Errorf("method = %q, want GET", req.Method) - } - if !strings.Contains(req.URL.String(), episodeQueryHash) { - t.Errorf("url should contain hash, got %q", req.URL.String()) + if req.Method != http.MethodPost { + t.Errorf("method = %q, want POST", req.Method) } if req.Header.Get("Referer") != allAnimeReferer { t.Errorf("Referer = %q", req.Header.Get("Referer")) } + if req.Header.Get("x-build-id") != "9" { + t.Errorf("x-build-id = %q", req.Header.Get("x-build-id")) + } return mockStringResponse(http.StatusOK, `{"data":{"episode":{"sourceUrls":[{"sourceUrl":"https://example.test/v.mp4","sourceName":"default"}]}}}`), nil }), }, @@ -190,7 +190,7 @@ func TestGraphqlRequestWithHash_Encrypted(t *testing.T) { encryptedPayload := buildEncryptedTobeparsedPayload(t, []byte(`{"sourceUrls":[{"sourceUrl":"https://e.test/v.mp4","sourceName":"default"}]}`)) provider := &AllAnimeProvider{ - utlsClient: &http.Client{ + httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return mockStringResponse(http.StatusOK, `{"data":{"tobeparsed":"`+encryptedPayload+`"}}`), nil }), @@ -205,7 +205,11 @@ func TestGraphqlRequestWithHash_Encrypted(t *testing.T) { t.Fatalf("graphqlRequestWithHash: %v", err) } - sources := nestedSlice(result, "episode", "sourceUrls") + ep, ok := result["episode"].(map[string]any) + if !ok { + t.Fatal("result missing episode key") + } + sources := nestedSlice(ep, "sourceUrls") if len(sources) != 1 { t.Fatalf("got %d sources, want 1", len(sources)) } @@ -215,7 +219,7 @@ func TestGraphqlRequestWithHash_Non200(t *testing.T) { t.Parallel() provider := &AllAnimeProvider{ - utlsClient: &http.Client{ + httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return mockStringResponse(http.StatusNotFound, `not found`), nil }), @@ -235,7 +239,7 @@ func TestGraphqlRequestWithHash_EmptyData(t *testing.T) { t.Parallel() provider := &AllAnimeProvider{ - utlsClient: &http.Client{ + httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return mockStringResponse(http.StatusOK, `{"data":{}}`), nil }), @@ -258,12 +262,6 @@ func TestGetEpisodeSources_EncryptedHash(t *testing.T) { provider := &AllAnimeProvider{ httpClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - t.Error("fallback POST should not be called") - return nil, nil - }), - }, - utlsClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return mockStringResponse(http.StatusOK, `{"data":{"tobeparsed":"`+encrypted+`"}}`), nil }), @@ -292,15 +290,15 @@ func TestGetEpisodeSources_FallbackPost(t *testing.T) { provider := &AllAnimeProvider{ httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + req.Body.Close() + if strings.Contains(string(body), `"extensions":`) { + return mockStringResponse(http.StatusOK, `{"data":{}}`), nil + } fallbackCalled = true return mockStringResponse(http.StatusOK, sourceResponse), nil }), }, - utlsClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return mockStringResponse(http.StatusNotFound, `not found`), nil - }), - }, extractor: newProviderExtractor(), } @@ -325,11 +323,6 @@ func TestGetEpisodeSources_BothFail(t *testing.T) { return mockStringResponse(http.StatusNotFound, `not found`), nil }), }, - utlsClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return mockStringResponse(http.StatusNotFound, `not found`), nil - }), - }, extractor: newProviderExtractor(), } @@ -464,11 +457,11 @@ func TestGetStreams_FullSuccess(t *testing.T) { provider := &AllAnimeProvider{ httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return mockStringResponse(http.StatusOK, searchBody), nil - }), - }, - utlsClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + req.Body.Close() + if strings.Contains(string(body), `"search":`) { + return mockStringResponse(http.StatusOK, searchBody), nil + } return mockStringResponse(http.StatusOK, `{"data":{"tobeparsed":"`+encrypted+`"}}`), nil }), }, @@ -499,12 +492,6 @@ func TestGetStreams_ShowNotFound(t *testing.T) { return mockStringResponse(http.StatusOK, `{"data":{"shows":{"edges":[]}}}`), nil }), }, - utlsClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - t.Error("should not call episode sources when show not found") - return nil, nil - }), - }, extractor: newProviderExtractor(), } @@ -520,11 +507,11 @@ func TestGetStreams_NoSources(t *testing.T) { provider := &AllAnimeProvider{ httpClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - return mockStringResponse(http.StatusOK, `{"data":{"shows":{"edges":[{"_id":"showX","malId":"1","name":"Anime"}]}}}`), nil - }), - }, - utlsClient: &http.Client{ - Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + req.Body.Close() + if strings.Contains(string(body), `"search":`) { + return mockStringResponse(http.StatusOK, `{"data":{"shows":{"edges":[{"_id":"showX","malId":"1","name":"Anime"}]}}}`), nil + } return mockStringResponse(http.StatusNotFound, `not found`), nil }), }, diff --git a/integrations/playback/allanime/sources.go b/integrations/playback/allanime/sources.go index 46b79cdf..5440f6d0 100644 --- a/integrations/playback/allanime/sources.go +++ b/integrations/playback/allanime/sources.go @@ -1,11 +1,12 @@ package allanime import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "net/http" - "net/url" "strings" ) @@ -172,11 +173,15 @@ func isHTTPURL(value string) bool { } func buildStreamSource(url, sourceType, provider string) StreamSource { + referer := allAnimeReferer + if strings.Contains(strings.ToLower(provider), "yt-mp4") { + referer = allAnimeSiteURL + } return StreamSource{ URL: url, Provider: provider, Type: sourceType, - Referer: allAnimeReferer, + Referer: referer, } } @@ -240,28 +245,24 @@ func stringMapValue(item map[string]any, key string) (string, bool) { } func (c *AllAnimeProvider) graphqlRequestWithHash(ctx context.Context, showID, episode, mode string) (map[string]any, error) { - req, err := newHashRequest(ctx, showID, episode, mode) + req, err := c.newHashRequest(ctx, showID, episode, mode) if err != nil { - return nil, fmt.Errorf("create GET request: %w", err) + return nil, fmt.Errorf("create POST request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", defaultUserAgent) - req.Header.Set("Accept", "*/*") - req.Header.Set("Accept-Language", "en-US,en;q=0.5") - req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Referer", allAnimeReferer) req.Header.Set("Origin", allAnimeOrigin) - req.Header.Set("Sec-Fetch-Dest", "empty") - req.Header.Set("Sec-Fetch-Mode", "cors") - req.Header.Set("Sec-Fetch-Site", "cross-site") + req.Header.Set("x-build-id", "9") - statusCode, respBody, err := executeAndReadResponse(c.utlsClient, req, "execute GET request", "read response") + statusCode, respBody, err := executeAndReadResponse(c.httpClient, req, "execute POST request (aaReq)", "read response") if err != nil { return nil, err } if statusCode != http.StatusOK { - return nil, fmt.Errorf("GET status %d: %s", statusCode, string(respBody)) + return nil, fmt.Errorf("POST status %d: %s", statusCode, string(respBody)) } parsed, err := parseGraphQLResponse(respBody, "decode response") @@ -289,15 +290,33 @@ func (c *AllAnimeProvider) graphqlRequestWithHash(ctx context.Context, showID, e return nil, errors.New("no usable data in response") } -func newHashRequest(ctx context.Context, showID, episode, mode string) (*http.Request, error) { - varsJSON := fmt.Sprintf(`{"showId":"%s","translationType":"%s","episodeString":"%s"}`, showID, strings.ToLower(mode), episode) - extJSON := fmt.Sprintf(`{"persistedQuery":{"version":1,"sha256Hash":"%s"}}`, episodeQueryHash) +func (c *AllAnimeProvider) newHashRequest(ctx context.Context, showID, episode, mode string) (*http.Request, error) { + aaReq, err := makeAALease(episodeQueryHash) + if err != nil { + return nil, fmt.Errorf("create aa lease: %w", err) + } - params := url.Values{} - params.Set("variables", varsJSON) - params.Set("extensions", extJSON) + payload := map[string]any{ + "variables": map[string]any{ + "showId": showID, + "translationType": strings.ToLower(mode), + "episodeString": episode, + }, + "extensions": map[string]any{ + "persistedQuery": map[string]any{ + "version": 1, + "sha256Hash": episodeQueryHash, + }, + "aaReq": aaReq, + }, + } - return http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api?%s", allAnimeBaseURL, params.Encode()), nil) + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshal payload: %w", err) + } + + return http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api", allAnimeBaseURL), bytes.NewReader(body)) } func detectStreamType(sourceURL string) string {