mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
Implement SignalR Core support and add session fetching for live timing
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -14,6 +17,132 @@ import (
|
||||
// to timing topics, and sends defensive snapshots on dataChan until the
|
||||
// connection closes.
|
||||
func ConnectToF1LiveTiming(dataChan chan LiveStreamData) error {
|
||||
if err := connectToF1SignalRCore(dataChan); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
log.Printf("f1 signalrcore feed unavailable, trying legacy signalr: %v", err)
|
||||
}
|
||||
return connectToF1LegacySignalR(dataChan)
|
||||
}
|
||||
|
||||
func connectToF1SignalRCore(dataChan chan LiveStreamData) error {
|
||||
req, err := http.NewRequest("POST", "https://livetiming.formula1.com/signalrcore/negotiate?negotiateVersion=1", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Origin", "https://www.formula1.com")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
req.Header.Set("Content-Length", "0")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("f1 signalrcore negotiate returned %s", resp.Status)
|
||||
}
|
||||
|
||||
var neg struct {
|
||||
ConnectionToken string `json:"connectionToken"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &neg); err != nil {
|
||||
return err
|
||||
}
|
||||
if neg.ConnectionToken == "" {
|
||||
return fmt.Errorf("f1 signalrcore negotiate returned an empty connection token")
|
||||
}
|
||||
|
||||
wsURL := "wss://livetiming.formula1.com/signalrcore?id=" + url.QueryEscape(neg.ConnectionToken)
|
||||
header := http.Header{}
|
||||
header.Set("Origin", "https://www.formula1.com")
|
||||
header.Set("User-Agent", "Mozilla/5.0")
|
||||
for _, cookie := range resp.Cookies() {
|
||||
header.Add("Cookie", cookie.String())
|
||||
}
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writeCoreFrame := func(payload any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = append(body, signalRRecordSeparator)
|
||||
return c.WriteMessage(websocket.TextMessage, body)
|
||||
}
|
||||
|
||||
if err := writeCoreFrame(map[string]any{"protocol": "json", "version": 1}); err != nil {
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
_, handshake, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
if !bytes.Contains(handshake, []byte("{}")) {
|
||||
c.Close()
|
||||
return fmt.Errorf("f1 signalrcore handshake returned %q", string(handshake))
|
||||
}
|
||||
|
||||
topics := []string{
|
||||
"Heartbeat",
|
||||
"TimingData",
|
||||
"DriverList",
|
||||
"LapCount",
|
||||
"ExtrapolatedClock",
|
||||
"TrackStatus",
|
||||
"RaceControlMessages",
|
||||
"WeatherData",
|
||||
"SessionInfo",
|
||||
"CurrentTyres",
|
||||
"TimingAppData",
|
||||
"TimingStats",
|
||||
"SessionStatus",
|
||||
"TopThree",
|
||||
}
|
||||
if err := writeCoreFrame(map[string]any{
|
||||
"type": 1,
|
||||
"target": "subscribe",
|
||||
"arguments": []any{topics},
|
||||
"invocationId": "1",
|
||||
}); err != nil {
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer c.Close()
|
||||
state := NewState()
|
||||
|
||||
for {
|
||||
_, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("SignalR Core read error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if state.ProcessCoreMessage(message) {
|
||||
select {
|
||||
case dataChan <- state.Snapshot():
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectToF1LegacySignalR(dataChan chan LiveStreamData) error {
|
||||
hubName := `[{"name":"Streaming"}]`
|
||||
negotiateURL := fmt.Sprintf("https://livetiming.formula1.com/signalr/negotiate?clientProtocol=1.5&connectionData=%s", url.QueryEscape(hubName))
|
||||
|
||||
@@ -21,20 +150,30 @@ func ConnectToF1LiveTiming(dataChan chan LiveStreamData) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "BestHTTP")
|
||||
if token := f1LiveBearerToken(); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cookies := resp.Cookies()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("f1 signalr negotiate returned %s (auth=%s)", resp.Status, authState())
|
||||
}
|
||||
|
||||
cookies := resp.Cookies()
|
||||
var neg struct {
|
||||
ConnectionToken string `json:"ConnectionToken"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&neg); err != nil {
|
||||
return err
|
||||
}
|
||||
if neg.ConnectionToken == "" {
|
||||
return fmt.Errorf("f1 signalr negotiate returned an empty connection token")
|
||||
}
|
||||
|
||||
wsURL := fmt.Sprintf("wss://livetiming.formula1.com/signalr/connect?clientProtocol=1.5&transport=webSockets&connectionToken=%s&connectionData=%s",
|
||||
url.QueryEscape(neg.ConnectionToken),
|
||||
@@ -46,6 +185,9 @@ func ConnectToF1LiveTiming(dataChan chan LiveStreamData) error {
|
||||
header.Add("Cookie", cookie.String())
|
||||
}
|
||||
header.Add("User-Agent", "BestHTTP")
|
||||
if token := f1LiveBearerToken(); token != "" {
|
||||
header.Add("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
|
||||
if err != nil {
|
||||
@@ -80,3 +222,17 @@ func ConnectToF1LiveTiming(dataChan chan LiveStreamData) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func f1LiveBearerToken() string {
|
||||
if token := os.Getenv("BOXBOX_F1_LIVE_BEARER_TOKEN"); token != "" {
|
||||
return token
|
||||
}
|
||||
return os.Getenv("F1_LIVE_BEARER_TOKEN")
|
||||
}
|
||||
|
||||
func authState() string {
|
||||
if f1LiveBearerToken() != "" {
|
||||
return "bearer-configured"
|
||||
}
|
||||
return "no-bearer-token"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user