Skip to content

Commit fb43fa1

Browse files
authored
Merge pull request #1083 from Zibbp/token-fallback
fix: fallback without auth if twitch token is invalid
2 parents 06b5f53 + 30d0a55 commit fb43fa1

File tree

1 file changed

+85
-32
lines changed

1 file changed

+85
-32
lines changed

internal/platform/twitch_gql.go

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net/http"
88
"strings"
9+
"time"
910

1011
"github.com/rs/zerolog/log"
1112
"github.com/zibbp/ganymede/internal/chapter"
@@ -14,7 +15,12 @@ import (
1415
)
1516

1617
type TwitchGQLPlaybackAccessTokenResponse struct {
17-
Data TwitchGQLPlaybackAccessTokenData `json:"data"`
18+
Data TwitchGQLPlaybackAccessTokenData `json:"data"`
19+
Errors []TwitchGQLError `json:"errors"`
20+
}
21+
22+
type TwitchGQLError struct {
23+
Message string `json:"message"`
1824
}
1925

2026
type TwitchGQLPlaybackAccessTokenData struct {
@@ -147,41 +153,75 @@ type TwitchGQLNodeVideo struct {
147153

148154
// GQLRequest sends a generic GQL request and returns the response.
149155
func twitchGQLRequest(body string) ([]byte, error) {
156+
return twitchGQLRequestWithAuth(body, true)
157+
}
158+
159+
// twitchGQLRequestWithAuth sends a generic GQL request and optionally includes the configured Twitch OAuth token.
160+
func twitchGQLRequestWithAuth(body string, includeAuth bool) ([]byte, error) {
150161
client := &http.Client{}
151-
req, err := http.NewRequest("POST", "https://gql.twitch.tv/gql", strings.NewReader(body))
152-
if err != nil {
153-
return nil, fmt.Errorf("error creating request: %w", err)
162+
const maxAttempts = 3
163+
164+
var lastErr error
165+
for attempt := 1; attempt <= maxAttempts; attempt++ {
166+
req, err := http.NewRequest("POST", "https://gql.twitch.tv/gql", strings.NewReader(body))
167+
if err != nil {
168+
return nil, fmt.Errorf("error creating request: %w", err)
169+
}
170+
171+
req.Header.Set("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko")
172+
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
173+
req.Header.Set("Origin", "https://www.twitch.tv")
174+
req.Header.Set("Referer", "https://www.twitch.tv/")
175+
req.Header.Set("Sec-Fetch-Mode", "cors")
176+
req.Header.Set("Sec-Fetch-Site", "same-site")
177+
req.Header.Set("User-Agent", utils.ChromeUserAgent)
178+
179+
twitchToken := config.Get().Parameters.TwitchToken
180+
if includeAuth && twitchToken != "" {
181+
req.Header.Set("Authorization", fmt.Sprintf("OAuth %s", twitchToken))
182+
}
183+
184+
resp, err := client.Do(req)
185+
if err != nil {
186+
lastErr = fmt.Errorf("error sending request: %w", err)
187+
} else {
188+
bodyBytes, readErr := io.ReadAll(resp.Body)
189+
if closeErr := resp.Body.Close(); closeErr != nil {
190+
log.Debug().Err(closeErr).Msg("error closing response body")
191+
}
192+
193+
if readErr != nil {
194+
lastErr = fmt.Errorf("error reading response body: %w", readErr)
195+
} else if resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusTooManyRequests {
196+
lastErr = fmt.Errorf("received retryable status code: %d", resp.StatusCode)
197+
} else {
198+
return bodyBytes, nil
199+
}
200+
}
201+
202+
if attempt < maxAttempts {
203+
time.Sleep(time.Duration(attempt) * 300 * time.Millisecond)
204+
}
154205
}
155206

156-
req.Header.Set("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko")
157-
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
158-
req.Header.Set("Origin", "https://www.twitch.tv")
159-
req.Header.Set("Referer", "https://www.twitch.tv/")
160-
req.Header.Set("Sec-Fetch-Mode", "cors")
161-
req.Header.Set("Sec-Fetch-Site", "same-site")
162-
req.Header.Set("User-Agent", utils.ChromeUserAgent)
207+
return nil, lastErr
208+
}
163209

164-
twitchToken := config.Get().Parameters.TwitchToken
165-
if twitchToken != "" {
166-
req.Header.Set("Authorization", fmt.Sprintf("OAuth %s", twitchToken))
210+
func parsePlaybackAccessTokenResponse(respBytes []byte) (*TwitchGQLPlaybackAccessToken, error) {
211+
var resp TwitchGQLPlaybackAccessTokenResponse
212+
if err := json.Unmarshal(respBytes, &resp); err != nil {
213+
return nil, fmt.Errorf("error unmarshalling playback access token response: %w", err)
167214
}
168215

169-
resp, err := client.Do(req)
170-
if err != nil {
171-
return nil, fmt.Errorf("error sending request: %w", err)
216+
if len(resp.Errors) > 0 {
217+
return nil, fmt.Errorf("gql playback access token error: %s", resp.Errors[0].Message)
172218
}
173-
defer func() {
174-
if err := resp.Body.Close(); err != nil {
175-
log.Debug().Err(err).Msg("error closing response body")
176-
}
177-
}()
178219

179-
bodyBytes, err := io.ReadAll(resp.Body)
180-
if err != nil {
181-
return nil, fmt.Errorf("error reading response body: %w", err)
220+
if resp.Data.StreamPlaybackAccessToken.Signature == "" || resp.Data.StreamPlaybackAccessToken.Value == "" {
221+
return nil, fmt.Errorf("empty playback access token response")
182222
}
183223

184-
return bodyBytes, nil
224+
return &resp.Data.StreamPlaybackAccessToken, nil
185225
}
186226

187227
func (c *TwitchConnection) TwitchGQLGetMutedSegments(id string) ([]TwitchGQLMutedSegment, error) {
@@ -247,18 +287,31 @@ func (c *TwitchConnection) TwitchGQLGetPlaybackAccessToken(channel string) (*Twi
247287
"query": "query PlaybackAccessToken($isLive: Boolean!, $login: String!, $isVod: Boolean!, $vodID: ID!, $playerType: String!) {\nstreamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) {\nvalue\nsignature\n}\nvideoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) {\nvalue\nsignature\n}\n}"
248288
}`, channel)
249289

250-
respBytes, err := twitchGQLRequest(body)
290+
twitchToken := config.Get().Parameters.TwitchToken
291+
if twitchToken != "" {
292+
respBytes, err := twitchGQLRequestWithAuth(body, true)
293+
if err != nil {
294+
log.Warn().Err(err).Msg("failed to get playback access token with Twitch OAuth token, retrying without token")
295+
} else {
296+
token, parseErr := parsePlaybackAccessTokenResponse(respBytes)
297+
if parseErr == nil {
298+
return token, nil
299+
}
300+
log.Warn().Err(parseErr).Msg("configured Twitch OAuth token appears invalid for playback access, retrying without token")
301+
}
302+
}
303+
304+
respBytes, err := twitchGQLRequestWithAuth(body, false)
251305
if err != nil {
252-
return nil, fmt.Errorf("error getting playback access token: %w", err)
306+
return nil, fmt.Errorf("error getting playback access token without oauth token: %w", err)
253307
}
254308

255-
var resp TwitchGQLPlaybackAccessTokenResponse
256-
err = json.Unmarshal(respBytes, &resp)
309+
token, err := parsePlaybackAccessTokenResponse(respBytes)
257310
if err != nil {
258-
return nil, fmt.Errorf("error unmarshalling playback access token response: %w", err)
311+
return nil, err
259312
}
260313

261-
return &resp.Data.StreamPlaybackAccessToken, nil
314+
return token, nil
262315
}
263316

264317
// convertTwitchChaptersToChapters converts Twitch chapters to chapters. Twitch chapters are in milliseconds.

0 commit comments

Comments
 (0)