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
1617type 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
2026type TwitchGQLPlaybackAccessTokenData struct {
@@ -147,41 +153,75 @@ type TwitchGQLNodeVideo struct {
147153
148154// GQLRequest sends a generic GQL request and returns the response.
149155func 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
187227func (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