Skip to content

Commit 26248ea

Browse files
committed
Removed python code, fixed issues with tires
1 parent c7dc416 commit 26248ea

43 files changed

Lines changed: 4079 additions & 14891 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ A web app for watching Formula 1 sessions with real timing data, car positions o
3232
## Architecture
3333

3434
- **Frontend**: React + Vite with Tailwind CSS, served by Nginx
35-
- **Backend**: Go web service - serves pre-computed data from local storage
36-
- **Data Source**: [FastF1](https://github.com/theOehrly/Fast-F1) (used during data processing only)
35+
- **Backend**: Go web service - serves pre-computed data from SQLite (`/data/f1.db`)
36+
- **Data Source**: Official F1 timing endpoints (`livetiming.formula1.com`) ingested directly by the Go backend
3737

38-
Session data is processed once and stored locally (or in R2 for remote access). You can either pre-compute data in bulk ahead of time, or let the app process sessions on demand when you select them.
38+
Session data is processed once and stored in SQLite (`/data/f1.db`). You can queue downloads in the UI, or let the app process sessions on demand when you select them.
3939

4040
## Self-Hosting Guide
4141

@@ -53,7 +53,6 @@ services:
5353
- DATA_DIR=/data
5454
volumes:
5555
- f1data:/data
56-
- f1cache:/data/fastf1-cache
5756

5857
frontend:
5958
image: ghcr.io/adn8naiagent/f1replaytiming-frontend:latest
@@ -64,7 +63,6 @@ services:
6463

6564
volumes:
6665
f1data:
67-
f1cache:
6866
```
6967
7068
Then run:
@@ -132,34 +130,24 @@ frontend:
132130
- `REPLAY_CACHE_MAX_MB` - maximum RAM budget for cached replay sessions in the Go backend (default `256`)
133131
- `REPLAY_CACHE_TTL_SECONDS` - how long an inactive replay session stays cached after last client disconnect (default `300`)
134132
- `REPLAY_SAMPLE_INTERVAL_SECONDS` - replay frame sampling interval during precompute (default `0.5`; higher values reduce CPU/RAM during downloads)
133+
- `SQLITE_PATH` - SQLite database path (default `/data/f1.db`)
134+
- `SQLITE_BUSY_TIMEOUT_MS` - SQLite lock wait timeout in milliseconds (default `5000`)
135+
- `REPLAY_CHUNK_FRAMES` - replay protobuf chunk frame count (default `256`)
136+
- `TELEMETRY_CHUNK_SAMPLES` - telemetry sampling target per lap (default `512`)
137+
- `PROCESS_CHUNK_CODEC` - chunk codec for replay/telemetry (`protobuf` or `protobuf+zstd`, default `protobuf`)
138+
- `PROCESS_RAW_MIN_DT_SECONDS` - minimum accepted delta between raw stream samples per driver during ingest (default `0.10`)
135139
- `GOMEMLIMIT` / `GOGC` - Go runtime GC tuning knobs for tighter memory limits (for example `GOMEMLIMIT=256MiB`, `GOGC=50`)
136140

137141
#### Data
138142

139143
Session data is persisted in a Docker volume, so it survives restarts.
140-
141-
To pre-process session data in bulk (instead of on demand), use the precompute script:
142-
143-
```bash
144-
# Process a specific race weekend
145-
docker compose exec backend python data-fetcher/precompute.py 2026 --round 1
146-
147-
# Process only the race session (skip practice/qualifying)
148-
docker compose exec backend python data-fetcher/precompute.py 2026 --round 1 --session R
149-
150-
# Process an entire season (will take several hours)
151-
docker compose exec backend python data-fetcher/precompute.py 2025 --skip-existing
152-
153-
# Process multiple years
154-
docker compose exec backend python data-fetcher/precompute.py 2024 2025 --skip-existing
155-
```
144+
Use the Downloads page in the UI to enqueue single sessions, weekends, or full-season ranges for background processing.
156145

157146
### Option C: Manual setup
158147

159148
#### Prerequisites
160149

161150
- Go 1.26+
162-
- Python 3.10+
163151
- Node.js 18+
164152

165153
#### 1. Clone the repository
@@ -175,8 +163,7 @@ cd F1timing
175163
```
176164
PORT=8000
177165
DATA_DIR=./data
178-
PY_WORKER_PATH=../data-fetcher/worker_bridge.py
179-
# PYTHON_BIN=python3
166+
SQLITE_PATH=./data/f1.db
180167

181168
# Optional - restrict access with a passphrase
182169
AUTH_ENABLED=false
@@ -200,9 +187,6 @@ AUTH_PASSPHRASE=
200187
#### 3. Install dependencies and start
201188
202189
```bash
203-
# Python data-fetcher dependencies (required for on-demand session processing)
204-
python3 -m pip install -r data-fetcher/requirements-worker.txt
205-
206190
# Backend (Go API)
207191
cd backend
208192
go run .
@@ -221,31 +205,11 @@ There are two ways to get session data into the app:
221205

222206
#### Option A: On-demand processing (recommended for getting started)
223207

224-
Simply select any past session from the homepage. If the data hasn't been processed yet, the app will automatically fetch and process it using FastF1 and start the replay. The first load of a session takes **1-3 minutes**. After that, it's instant.
208+
Select any past session from the homepage. If the data has not been processed yet, the backend will fetch and process it directly in Go, then store it in SQLite. The first load typically takes **1-3 minutes**.
225209

226-
#### Option B: Bulk pre-compute (recommended for preparing a full season)
210+
#### Option B: Queue background downloads (recommended for preparing a full season)
227211

228-
Use the CLI script to process sessions ahead of time. This is useful if you want all data ready before you start using the app.
229-
230-
```bash
231-
# Data fetcher (Python)
232-
cd data-fetcher
233-
python -m venv venv
234-
source venv/bin/activate
235-
pip install -r requirements-worker.txt
236-
237-
# Process a specific race weekend
238-
python precompute.py 2026 --round 1
239-
240-
# Process only the race session (skip practice/qualifying)
241-
python precompute.py 2026 --round 1 --session R
242-
243-
# Process an entire season (will take several hours)
244-
python precompute.py 2025 --skip-existing
245-
246-
# Process multiple years
247-
python precompute.py 2024 2025 --skip-existing
248-
```
212+
Use the Downloads page to enqueue sessions ahead of time (single session, weekend, or season scopes). The queue runs automatically in the backend.
249213

250214
**Timing estimates:**
251215
- A single session (e.g. one race) takes **1-3 minutes**
@@ -256,7 +220,7 @@ The app also includes a background task that automatically checks for and proces
256220

257221
## Acknowledgements
258222

259-
This project is powered by [FastF1](https://github.com/theOehrly/Fast-F1), an open-source Python library for accessing Formula 1 timing and telemetry data. FastF1 is the original inspiration and data source for this project - without it, none of this would be possible.
223+
Thanks to the broader motorsport telemetry/open-source community and tools that informed earlier iterations of this project.
260224

261225
## License
262226

backend/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ COPY --from=builder /out/f1-go-backend /f1-go-backend
1313

1414
ENV PORT=8000
1515
ENV DATA_DIR=/data
16-
ENV PROCESSOR_MODE=go
16+
ENV SQLITE_PATH=/data/f1.db
17+
ENV SQLITE_BUSY_TIMEOUT_MS=5000
18+
ENV REPLAY_CHUNK_FRAMES=256
19+
ENV TELEMETRY_CHUNK_SAMPLES=512
20+
ENV PROCESS_CHUNK_CODEC=protobuf
21+
ENV PROCESS_RAW_MIN_DT_SECONDS=0.10
1722

1823
EXPOSE 8000
1924

backend/downloads.go

Lines changed: 22 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
package main
22

33
import (
4-
"bufio"
54
"context"
65
"encoding/json"
76
"errors"
87
"fmt"
98
"log"
109
"net/http"
1110
"os"
12-
"os/exec"
13-
"path/filepath"
1411
"sort"
1512
"strconv"
1613
"strings"
@@ -112,7 +109,6 @@ type queueSnapshot struct {
112109

113110
type downloadManager struct {
114111
app *app
115-
statePath string
116112
timeout time.Duration
117113
maxAttempts int
118114
retryBase time.Duration
@@ -131,7 +127,6 @@ func newDownloadManager(app *app, dataDir string) *downloadManager {
131127

132128
m := &downloadManager{
133129
app: app,
134-
statePath: filepath.Join(dataDir, "downloads", "state.json"),
135130
timeout: time.Duration(timeoutMinutes) * time.Minute,
136131
maxAttempts: maxAttempts,
137132
retryBase: time.Duration(retryBaseSec) * time.Second,
@@ -343,7 +338,10 @@ func cloneJob(job *downloadJob) *downloadJob {
343338
}
344339

345340
func (m *downloadManager) loadState() {
346-
b, err := os.ReadFile(m.statePath)
341+
if m.app.store == nil {
342+
return
343+
}
344+
b, err := m.app.store.LoadDownloadStateBlob(context.Background())
347345
if err != nil {
348346
return
349347
}
@@ -410,24 +408,16 @@ func (m *downloadManager) persistLocked() {
410408
Active: m.active,
411409
Recent: m.recent,
412410
}
413-
b, err := json.MarshalIndent(state, "", " ")
411+
b, err := json.Marshal(state)
414412
if err != nil {
415413
log.Printf("downloads: persist marshal failed: %v", err)
416414
return
417415
}
418-
419-
dir := filepath.Dir(m.statePath)
420-
if err := os.MkdirAll(dir, 0o755); err != nil {
421-
log.Printf("downloads: persist mkdir failed: %v", err)
416+
if m.app.store == nil {
422417
return
423418
}
424-
tmp := m.statePath + ".tmp"
425-
if err := os.WriteFile(tmp, b, 0o644); err != nil {
426-
log.Printf("downloads: persist write failed: %v", err)
427-
return
428-
}
429-
if err := os.Rename(tmp, m.statePath); err != nil {
430-
log.Printf("downloads: persist rename failed: %v", err)
419+
if err := m.app.store.SaveDownloadStateBlob(context.Background(), b); err != nil {
420+
log.Printf("downloads: persist sqlite failed: %v", err)
431421
}
432422
}
433423

@@ -653,106 +643,29 @@ func (m *downloadManager) failedKeysByYear(year int) []sessionKey {
653643
}
654644

655645
func (a *app) runProcessSessionWorker(ctx context.Context, year, round int, sessionType string, onStatus func(string)) error {
656-
if a.processor != nil {
657-
return a.processor.ProcessSession(ctx, year, round, sessionType, onStatus)
658-
}
659-
args := []string{
660-
a.workerPath,
661-
"process-session",
662-
"--year", strconv.Itoa(year),
663-
"--round", strconv.Itoa(round),
664-
"--type", strings.ToUpper(strings.TrimSpace(sessionType)),
665-
}
666-
cmd := exec.CommandContext(ctx, a.pythonBin, args...)
667-
cmd.Env = os.Environ()
668-
669-
stdout, err := cmd.StdoutPipe()
670-
if err != nil {
671-
return err
646+
if a.processor == nil {
647+
return errors.New("session processor is not initialized")
672648
}
673-
stderr, err := cmd.StderrPipe()
674-
if err != nil {
675-
return err
676-
}
677-
678-
if err := cmd.Start(); err != nil {
679-
return err
680-
}
681-
682-
var wg sync.WaitGroup
683-
var mu sync.Mutex
684-
var statusErr error
685-
686-
wg.Add(1)
687-
go func() {
688-
defer wg.Done()
689-
s := bufio.NewScanner(stderr)
690-
buf := make([]byte, 0, 64*1024)
691-
s.Buffer(buf, 1024*1024)
692-
for s.Scan() {
693-
log.Printf("worker stderr: %s", s.Text())
694-
}
695-
}()
696-
697-
wg.Add(1)
698-
go func() {
699-
defer wg.Done()
700-
s := bufio.NewScanner(stdout)
701-
buf := make([]byte, 0, 64*1024)
702-
s.Buffer(buf, 1024*1024)
703-
for s.Scan() {
704-
line := s.Bytes()
705-
var evt map[string]any
706-
if err := json.Unmarshal(line, &evt); err != nil {
707-
log.Printf("worker out: %s", string(line))
708-
continue
709-
}
710-
typ := asString(evt["type"])
711-
if typ == "status" && onStatus != nil {
712-
onStatus(asString(evt["message"]))
713-
}
714-
if typ == "error" {
715-
mu.Lock()
716-
statusErr = errors.New(defaultString(asString(evt["message"]), "worker failed"))
717-
mu.Unlock()
718-
}
719-
}
720-
}()
721-
722-
waitErr := cmd.Wait()
723-
wg.Wait()
724-
725-
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
726-
return ctx.Err()
727-
}
728-
729-
mu.Lock()
730-
errCopy := statusErr
731-
mu.Unlock()
732-
if errCopy != nil {
733-
return errCopy
734-
}
735-
if waitErr != nil {
736-
if ctx.Err() != nil {
737-
return ctx.Err()
738-
}
739-
return waitErr
740-
}
741-
return nil
649+
return a.processor.ProcessSession(ctx, year, round, sessionType, onStatus)
742650
}
743651

744652
func (a *app) isSessionDownloaded(year, round int, sessionType string) bool {
745-
base := filepath.Join("sessions", strconv.Itoa(year), strconv.Itoa(round), strings.ToUpper(strings.TrimSpace(sessionType)))
746-
return a.fileExists(filepath.Join(base, "replay.json")) && a.fileExists(filepath.Join(base, "info.json"))
653+
if a.store == nil {
654+
return false
655+
}
656+
ready, _, err := a.store.SessionReady(context.Background(), year, round, strings.ToUpper(strings.TrimSpace(sessionType)))
657+
return err == nil && ready
747658
}
748659

749660
func (a *app) sessionDataUpdatedAt(year, round int, sessionType string) (time.Time, bool) {
750-
p := filepath.Join(a.dataDir, "sessions", strconv.Itoa(year), strconv.Itoa(round), strings.ToUpper(strings.TrimSpace(sessionType)), "replay.json")
751-
st, err := os.Stat(p)
752-
if err != nil {
661+
if a.store == nil {
662+
return time.Time{}, false
663+
}
664+
_, updated, err := a.store.SessionReady(context.Background(), year, round, strings.ToUpper(strings.TrimSpace(sessionType)))
665+
if err != nil || updated.IsZero() {
753666
return time.Time{}, false
754667
}
755-
return st.ModTime(), true
668+
return updated, true
756669
}
757670

758671
func (a *app) sessionDownloadStatus(year, round int, sessionType string) sessionDownloadStatus {

backend/go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,19 @@ module f1replaytiming/backend
33
go 1.26.0
44

55
require github.com/gorilla/websocket v1.5.3
6+
7+
require (
8+
github.com/dustin/go-humanize v1.0.1 // indirect
9+
github.com/golang/protobuf v1.5.4 // indirect
10+
github.com/google/uuid v1.6.0 // indirect
11+
github.com/klauspost/compress v1.18.5 // indirect
12+
github.com/mattn/go-isatty v0.0.20 // indirect
13+
github.com/ncruces/go-strftime v1.0.0 // indirect
14+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
15+
golang.org/x/sys v0.42.0 // indirect
16+
google.golang.org/protobuf v1.33.0 // indirect
17+
modernc.org/libc v1.70.0 // indirect
18+
modernc.org/mathutil v1.7.1 // indirect
19+
modernc.org/memory v1.11.0 // indirect
20+
modernc.org/sqlite v1.48.0 // indirect
21+
)

backend/go.sum

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,29 @@
1+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
4+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
5+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
17
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
28
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9+
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
10+
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
11+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
12+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
13+
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
14+
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
15+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
16+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
17+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
19+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
20+
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
21+
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
22+
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
23+
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
24+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
25+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
26+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
27+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
28+
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
29+
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=

0 commit comments

Comments
 (0)