|
|
@@ -9,6 +9,7 @@ import (
|
|
|
|
"errors"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"math/rand"
|
|
|
|
"net/url"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
@@ -60,11 +61,11 @@ func (c *Client) SetProxy(proxy string) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) ExchangeCookie(ctx context.Context, cookie string) (*CookieExchangeResult, error) {
|
|
|
|
func (c *Client) ExchangeCookie(ctx context.Context, cookie string) (*CookieExchangeResult, error) {
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return exchangeCookieWithTLSClient(ctx, client, cookie)
|
|
|
|
return exchangeCookieWithTLSClient(ctx, sess, cookie)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// uploadMaxRetries is how many extra in-place attempts a transient upload
|
|
|
|
// uploadMaxRetries is how many extra in-place attempts a transient upload
|
|
|
@@ -106,7 +107,7 @@ func (c *Client) UploadImage(ctx context.Context, token string, content []byte,
|
|
|
|
// body plus whether a failure is retryable (transport error / 429/451/5xx).
|
|
|
|
// body plus whether a failure is retryable (transport error / 429/451/5xx).
|
|
|
|
// Auth failures (401/403) and other non-200s are not retryable.
|
|
|
|
// Auth failures (401/403) and other non-200s are not retryable.
|
|
|
|
func (c *Client) uploadImageOnce(ctx context.Context, token string, content []byte, contentType, engine string) ([]byte, error, bool) {
|
|
|
|
func (c *Client) uploadImageOnce(ctx context.Context, token string, content []byte, contentType, engine string) ([]byte, error, bool) {
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err, false
|
|
|
|
return nil, err, false
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -125,7 +126,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
|
|
|
|
"x-api-key": {c.apiKey},
|
|
|
|
"x-api-key": {c.apiKey},
|
|
|
|
"content-type": {defaultString(contentType, "image/png")},
|
|
|
|
"content-type": {defaultString(contentType, "image/png")},
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
|
"x-api-key",
|
|
|
|
"x-api-key",
|
|
|
@@ -135,7 +136,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
// Transport error (incl. Client.Timeout on the storage endpoint) — a
|
|
|
|
// Transport error (incl. Client.Timeout on the storage endpoint) — a
|
|
|
|
// network blip, retryable on a fresh connection.
|
|
|
|
// network blip, retryable on a fresh connection.
|
|
|
@@ -160,7 +161,7 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspectRatio, resolution string, blobIDs []string) ([]byte, map[string]any, error) {
|
|
|
|
func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspectRatio, resolution string, blobIDs []string) ([]byte, map[string]any, error) {
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -178,9 +179,9 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
|
|
|
|
candidates = BuildImagePayloadCandidates(modelID, prompt, aspectRatio, resolution, blobIDs)
|
|
|
|
candidates = BuildImagePayloadCandidates(modelID, prompt, aspectRatio, resolution, blobIDs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, payload := range candidates {
|
|
|
|
for _, payload := range candidates {
|
|
|
|
respBody, pollURL, err := c.submitImage(ctx, client, token, prompt, endpoint, payload)
|
|
|
|
respBody, pollURL, err := c.submitImage(ctx, sess, token, prompt, endpoint, payload)
|
|
|
|
if err == nil {
|
|
|
|
if err == nil {
|
|
|
|
meta, data, pollErr := c.pollImage(ctx, client, token, pollURL)
|
|
|
|
meta, data, pollErr := c.pollImage(ctx, sess, token, pollURL)
|
|
|
|
if pollErr != nil {
|
|
|
|
if pollErr != nil {
|
|
|
|
return nil, nil, pollErr
|
|
|
|
return nil, nil, pollErr
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -208,7 +209,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
|
|
|
|
// in meta["video_url"] — used by the async /v1/videos job, which proxies that URL
|
|
|
|
// in meta["video_url"] — used by the async /v1/videos job, which proxies that URL
|
|
|
|
// on /content instead of persisting the file.
|
|
|
|
// on /content instead of persisting the file.
|
|
|
|
func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspectRatio string, durationSeconds int, resolution, referenceMode, upstreamModel string, blobIDs []string, downloadResult bool) ([]byte, map[string]any, error) {
|
|
|
|
func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspectRatio string, durationSeconds int, resolution, referenceMode, upstreamModel string, blobIDs []string, downloadResult bool) ([]byte, map[string]any, error) {
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -218,12 +219,12 @@ func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspec
|
|
|
|
if engine == "firefly-video" {
|
|
|
|
if engine == "firefly-video" {
|
|
|
|
endpoint = fireflyVideoSubmitURL
|
|
|
|
endpoint = fireflyVideoSubmitURL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
respBody, pollURL, err := c.submitVideo(ctx, client, token, endpoint, payload)
|
|
|
|
respBody, pollURL, err := c.submitVideo(ctx, sess, token, endpoint, payload)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ = respBody
|
|
|
|
_ = respBody
|
|
|
|
meta, data, pollErr := c.pollVideo(ctx, client, token, pollURL, downloadResult)
|
|
|
|
meta, data, pollErr := c.pollVideo(ctx, sess, token, pollURL, downloadResult)
|
|
|
|
if pollErr != nil {
|
|
|
|
if pollErr != nil {
|
|
|
|
return nil, nil, pollErr
|
|
|
|
return nil, nil, pollErr
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -235,7 +236,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
|
|
|
|
if token == "" {
|
|
|
|
if token == "" {
|
|
|
|
return map[string]any{}, nil
|
|
|
|
return map[string]any{}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -249,7 +250,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
|
|
|
|
req.Header = http.Header{
|
|
|
|
req.Header = http.Header{
|
|
|
|
"authorization": {"Bearer " + token},
|
|
|
|
"authorization": {"Bearer " + token},
|
|
|
|
"accept": {"application/json"},
|
|
|
|
"accept": {"application/json"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
|
"accept",
|
|
|
|
"accept",
|
|
|
@@ -257,7 +258,7 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -320,7 +321,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
|
|
|
|
}, nil
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client, err := c.newTLSClient()
|
|
|
|
sess, err := c.newTLSClient()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -335,7 +336,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
|
|
|
|
"x-account-id": {accountID},
|
|
|
|
"x-account-id": {accountID},
|
|
|
|
"accept": {"application/json"},
|
|
|
|
"accept": {"application/json"},
|
|
|
|
"content-type": {"application/json"},
|
|
|
|
"content-type": {"application/json"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
|
"x-api-key",
|
|
|
|
"x-api-key",
|
|
|
@@ -346,7 +347,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return map[string]any{
|
|
|
|
return map[string]any{
|
|
|
|
"remaining": nil,
|
|
|
|
"remaining": nil,
|
|
|
@@ -401,7 +402,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
|
|
|
|
}, nil
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, token, prompt, endpoint string, payload map[string]any) ([]byte, string, error) {
|
|
|
|
func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, prompt, endpoint string, payload map[string]any) ([]byte, string, error) {
|
|
|
|
body, err := json.Marshal(payload)
|
|
|
|
body, err := json.Marshal(payload)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
return nil, "", err
|
|
|
@@ -419,13 +420,13 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"accept-language": {"en-US,en;q=0.9"},
|
|
|
|
"accept-language": {"en-US,en;q=0.9"},
|
|
|
|
"sec-ch-ua": {defaultSecCHUA},
|
|
|
|
"sec-ch-ua": {sess.fp.secCHUA},
|
|
|
|
"sec-ch-ua-mobile": {"?0"},
|
|
|
|
"sec-ch-ua-mobile": {"?0"},
|
|
|
|
"sec-ch-ua-platform": {`"Windows"`},
|
|
|
|
"sec-ch-ua-platform": {sess.fp.platform},
|
|
|
|
"sec-fetch-site": {"same-site"},
|
|
|
|
"sec-fetch-site": {"same-site"},
|
|
|
|
"sec-fetch-mode": {"cors"},
|
|
|
|
"sec-fetch-mode": {"cors"},
|
|
|
|
"sec-fetch-dest": {"empty"},
|
|
|
|
"sec-fetch-dest": {"empty"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
"x-arp-session-id": {buildARPSessionID()},
|
|
|
|
"x-arp-session-id": {buildARPSessionID()},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
@@ -450,7 +451,7 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
req.Header.Set("x-nonce", nonce)
|
|
|
|
req.Header.Set("x-nonce", nonce)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -497,7 +498,7 @@ func (c *Client) submitImage(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
return respBody, "", errors.New("submit ok but no poll url")
|
|
|
|
return respBody, "", errors.New("submit ok but no poll url")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, token, pollURL string) (map[string]any, []byte, error) {
|
|
|
|
func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL string) (map[string]any, []byte, error) {
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
for {
|
|
|
|
for {
|
|
|
|
if time.Since(start) > 3*time.Minute {
|
|
|
|
if time.Since(start) > 3*time.Minute {
|
|
|
@@ -514,7 +515,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
|
"accept",
|
|
|
|
"accept",
|
|
|
@@ -524,7 +525,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -551,7 +552,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
if first, ok := outputs[0].(map[string]any); ok {
|
|
|
|
if first, ok := outputs[0].(map[string]any); ok {
|
|
|
|
if image, ok := first["image"].(map[string]any); ok {
|
|
|
|
if image, ok := first["image"].(map[string]any); ok {
|
|
|
|
if url := strings.TrimSpace(stringValue(image["presignedUrl"])); url != "" {
|
|
|
|
if url := strings.TrimSpace(stringValue(image["presignedUrl"])); url != "" {
|
|
|
|
data, err := c.download(ctx, client, url)
|
|
|
|
data, err := c.download(ctx, sess, url)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -569,7 +570,7 @@ func (c *Client) pollImage(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, token, endpoint string, payload map[string]any) ([]byte, string, error) {
|
|
|
|
func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpoint string, payload map[string]any) ([]byte, string, error) {
|
|
|
|
body, err := json.Marshal(payload)
|
|
|
|
body, err := json.Marshal(payload)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
return nil, "", err
|
|
|
@@ -587,13 +588,13 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"accept-language": {"en-US,en;q=0.9"},
|
|
|
|
"accept-language": {"en-US,en;q=0.9"},
|
|
|
|
"sec-ch-ua": {defaultSecCHUA},
|
|
|
|
"sec-ch-ua": {sess.fp.secCHUA},
|
|
|
|
"sec-ch-ua-mobile": {"?0"},
|
|
|
|
"sec-ch-ua-mobile": {"?0"},
|
|
|
|
"sec-ch-ua-platform": {`"Windows"`},
|
|
|
|
"sec-ch-ua-platform": {sess.fp.platform},
|
|
|
|
"sec-fetch-site": {"same-site"},
|
|
|
|
"sec-fetch-site": {"same-site"},
|
|
|
|
"sec-fetch-mode": {"cors"},
|
|
|
|
"sec-fetch-mode": {"cors"},
|
|
|
|
"sec-fetch-dest": {"empty"},
|
|
|
|
"sec-fetch-dest": {"empty"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
"x-arp-session-id": {buildARPSessionID()},
|
|
|
|
"x-arp-session-id": {buildARPSessionID()},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
@@ -621,7 +622,7 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
return nil, "", fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -671,7 +672,7 @@ func (c *Client) submitVideo(ctx context.Context, client tlsclient.HttpClient, t
|
|
|
|
return respBody, "", errors.New("video submit ok but no poll url")
|
|
|
|
return respBody, "", errors.New("video submit ok but no poll url")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, token, pollURL string, downloadResult bool) (map[string]any, []byte, error) {
|
|
|
|
func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL string, downloadResult bool) (map[string]any, []byte, error) {
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
for {
|
|
|
|
for {
|
|
|
|
if time.Since(start) > 10*time.Minute {
|
|
|
|
if time.Since(start) > 10*time.Minute {
|
|
|
@@ -688,7 +689,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"authorization",
|
|
|
|
"authorization",
|
|
|
|
"accept",
|
|
|
|
"accept",
|
|
|
@@ -698,7 +699,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -732,7 +733,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
if !downloadResult {
|
|
|
|
if !downloadResult {
|
|
|
|
return payload, nil, nil
|
|
|
|
return payload, nil, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data, err := c.download(ctx, client, raw)
|
|
|
|
data, err := c.download(ctx, sess, raw)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -754,7 +755,7 @@ func (c *Client) pollVideo(ctx context.Context, client tlsclient.HttpClient, tok
|
|
|
|
// asset is already produced at this point, so a transient network hiccup
|
|
|
|
// asset is already produced at this point, so a transient network hiccup
|
|
|
|
// (connection EOF/reset mid-body, S3 accelerate 5xx) must not fail the whole
|
|
|
|
// (connection EOF/reset mid-body, S3 accelerate 5xx) must not fail the whole
|
|
|
|
// generation — retry with backoff before giving up.
|
|
|
|
// generation — retry with backoff before giving up.
|
|
|
|
func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url string) ([]byte, error) {
|
|
|
|
func (c *Client) download(ctx context.Context, sess *tlsSession, url string) ([]byte, error) {
|
|
|
|
var data []byte
|
|
|
|
var data []byte
|
|
|
|
var err error
|
|
|
|
var err error
|
|
|
|
for _, wait := range []time.Duration{0, 1 * time.Second, 2 * time.Second, 5 * time.Second, 10 * time.Second} {
|
|
|
|
for _, wait := range []time.Duration{0, 1 * time.Second, 2 * time.Second, 5 * time.Second, 10 * time.Second} {
|
|
|
@@ -765,7 +766,7 @@ func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url
|
|
|
|
case <-time.After(wait):
|
|
|
|
case <-time.After(wait):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data, err = c.downloadOnce(ctx, client, url)
|
|
|
|
data, err = c.downloadOnce(ctx, sess, url)
|
|
|
|
if err == nil || !errors.Is(err, ErrTemporaryUpstream) {
|
|
|
|
if err == nil || !errors.Is(err, ErrTemporaryUpstream) {
|
|
|
|
break
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -773,7 +774,7 @@ func (c *Client) download(ctx context.Context, client tlsclient.HttpClient, url
|
|
|
|
return data, err
|
|
|
|
return data, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient, url string) ([]byte, error) {
|
|
|
|
func (c *Client) downloadOnce(ctx context.Context, sess *tlsSession, url string) ([]byte, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
return nil, err
|
|
|
@@ -781,13 +782,13 @@ func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient,
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
req.Header = http.Header{
|
|
|
|
req.Header = http.Header{
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"accept": {"*/*"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"accept",
|
|
|
|
"accept",
|
|
|
|
"user-agent",
|
|
|
|
"user-agent",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
// Network-level failure (EOF, reset, timeout) — transient, worth a retry.
|
|
|
|
// Network-level failure (EOF, reset, timeout) — transient, worth a retry.
|
|
|
|
return nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
|
return nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
|
|
@@ -808,20 +809,78 @@ func (c *Client) downloadOnce(ctx context.Context, client tlsclient.HttpClient,
|
|
|
|
return data, nil
|
|
|
|
return data, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) newTLSClient() (tlsclient.HttpClient, error) {
|
|
|
|
// fingerprint bundles a TLS ClientProfile with the browser headers that match
|
|
|
|
|
|
|
|
// it, so the JA3/JA4 handshake and the advertised User-Agent / client hints
|
|
|
|
|
|
|
|
// stay internally consistent within a single request.
|
|
|
|
|
|
|
|
type fingerprint struct {
|
|
|
|
|
|
|
|
profile profiles.ClientProfile
|
|
|
|
|
|
|
|
userAgent string
|
|
|
|
|
|
|
|
secCHUA string
|
|
|
|
|
|
|
|
platform string // quoted sec-ch-ua-platform value, e.g. `"Windows"`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
|
|
osWindows = iota
|
|
|
|
|
|
|
|
osMac
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func newChromeFingerprint(profile profiles.ClientProfile, major, osKind int) fingerprint {
|
|
|
|
|
|
|
|
ua := fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", major)
|
|
|
|
|
|
|
|
platform := `"Windows"`
|
|
|
|
|
|
|
|
if osKind == osMac {
|
|
|
|
|
|
|
|
ua = fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", major)
|
|
|
|
|
|
|
|
platform = `"macOS"`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return fingerprint{
|
|
|
|
|
|
|
|
profile: profile,
|
|
|
|
|
|
|
|
userAgent: ua,
|
|
|
|
|
|
|
|
secCHUA: fmt.Sprintf(`"Not?A_Brand";v="99", "Google Chrome";v="%d", "Chromium";v="%d"`, major, major),
|
|
|
|
|
|
|
|
platform: platform,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// adobeFingerprints is a small pool of recent desktop Chrome builds across
|
|
|
|
|
|
|
|
// Windows/macOS. Each request draws one at random so the outbound TLS
|
|
|
|
|
|
|
|
// fingerprint and headers vary from call to call instead of presenting a single
|
|
|
|
|
|
|
|
// static signature to Adobe's anti-bot layer.
|
|
|
|
|
|
|
|
var adobeFingerprints = []fingerprint{
|
|
|
|
|
|
|
|
newChromeFingerprint(profiles.Chrome_133, 133, osWindows),
|
|
|
|
|
|
|
|
newChromeFingerprint(profiles.Chrome_131, 131, osWindows),
|
|
|
|
|
|
|
|
newChromeFingerprint(profiles.Chrome_124, 124, osWindows),
|
|
|
|
|
|
|
|
newChromeFingerprint(profiles.Chrome_133, 133, osMac),
|
|
|
|
|
|
|
|
newChromeFingerprint(profiles.Chrome_131, 131, osMac),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func randomFingerprint() fingerprint {
|
|
|
|
|
|
|
|
return adobeFingerprints[rand.Intn(len(adobeFingerprints))]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// tlsSession pairs a configured TLS client with the fingerprint it was built
|
|
|
|
|
|
|
|
// for, so downstream header builders advertise the matching UA / client hints.
|
|
|
|
|
|
|
|
type tlsSession struct {
|
|
|
|
|
|
|
|
client tlsclient.HttpClient
|
|
|
|
|
|
|
|
fp fingerprint
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) newTLSClient() (*tlsSession, error) {
|
|
|
|
|
|
|
|
fp := randomFingerprint()
|
|
|
|
options := []tlsclient.HttpClientOption{
|
|
|
|
options := []tlsclient.HttpClientOption{
|
|
|
|
tlsclient.WithTimeoutSeconds(60),
|
|
|
|
tlsclient.WithTimeoutSeconds(60),
|
|
|
|
tlsclient.WithClientProfile(profiles.Chrome_133),
|
|
|
|
tlsclient.WithClientProfile(fp.profile),
|
|
|
|
tlsclient.WithNotFollowRedirects(),
|
|
|
|
tlsclient.WithNotFollowRedirects(),
|
|
|
|
tlsclient.WithRandomTLSExtensionOrder(),
|
|
|
|
tlsclient.WithRandomTLSExtensionOrder(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if c.proxy != "" {
|
|
|
|
if c.proxy != "" {
|
|
|
|
options = append(options, tlsclient.WithProxyUrl(c.proxy))
|
|
|
|
options = append(options, tlsclient.WithProxyUrl(c.proxy))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...)
|
|
|
|
client, err := tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tlsSession{client: client, fp: fp}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClient, cookie string) (*CookieExchangeResult, error) {
|
|
|
|
func exchangeCookieWithTLSClient(ctx context.Context, sess *tlsSession, cookie string) (*CookieExchangeResult, error) {
|
|
|
|
cookie = normalizeCookie(cookie)
|
|
|
|
cookie = normalizeCookie(cookie)
|
|
|
|
if cookie == "" {
|
|
|
|
if cookie == "" {
|
|
|
|
return nil, ErrAdobeCookieEmpty
|
|
|
|
return nil, ErrAdobeCookieEmpty
|
|
|
@@ -840,7 +899,7 @@ func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClien
|
|
|
|
"cookie": {cookie},
|
|
|
|
"cookie": {cookie},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"origin": {"https://firefly.adobe.com"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"referer": {"https://firefly.adobe.com/"},
|
|
|
|
"user-agent": {defaultUserAgent},
|
|
|
|
"user-agent": {sess.fp.userAgent},
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
http.HeaderOrderKey: {
|
|
|
|
"accept",
|
|
|
|
"accept",
|
|
|
|
"accept-language",
|
|
|
|
"accept-language",
|
|
|
@@ -851,7 +910,7 @@ func exchangeCookieWithTLSClient(ctx context.Context, client tlsclient.HttpClien
|
|
|
|
"user-agent",
|
|
|
|
"user-agent",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
resp, err := sess.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("adobe cookie exchange network error: %w", err)
|
|
|
|
return nil, fmt.Errorf("adobe cookie exchange network error: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|