Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=f1timingdata

# Auto-precompute: which session types to fetch in the background during race weekends.
# Runs every 30 minutes Fri–Mon and downloads any matching session as soon as F1
# publishes data. Sessions not in this set are still fetched on-demand the first
# time you click them (with the usual 1–3 minute wait).
# off — disable, fetch everything on-demand only
# race — race + sprint
# race+qual — race, sprint, qualifying, sprint qualifying (default)
# all — every session including practice
AUTO_PRECOMPUTE=race+qual

# Optional — authentication
# AUTH_ENABLED=true
# AUTH_PASSPHRASE=your-passphrase
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to F1 Replay Timing will be documented in this file.

## 2.0.1

### Improvements
- **Broadcast delay up to 5 minutes** — Live Timing Offset slider now extends to -5m (previously -60s) to accommodate longer streaming-service lag, with M:SS formatting and ±30s quick-adjust buttons
- **Configurable auto-precompute** — new `AUTO_PRECOMPUTE` env var controls which session types the background task fetches during race weekends. Accepts `off`, `race`, `race+qual` (default), or `all`. Self-hosters who don't watch practice can avoid downloading FP1/FP2/FP3 data; sessions outside the configured set are still available on-demand

---

## 2.0.0

### Migrating from v1.x
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,16 @@ docker compose exec f1timing python precompute.py 2024 2025 --skip-existing
- A full race weekend (FP1, FP2, FP3, Qualifying, Race) takes **3-5 minutes**
- A complete season (~24 rounds, all sessions) takes **2-3 hours**

The app also includes a background task that automatically checks for and processes new session data on race weekends (Friday-Monday).
**Background auto-precompute:** On race weekends (Fri–Mon), a background task checks every 30 minutes for new session data and processes it ahead of time so the first click is instant. Which session types it fetches is controlled by the `AUTO_PRECOMPUTE` env var:

| Value | What it fetches |
|---|---|
| `off` | Nothing — everything is fetched on-demand the first time you click it |
| `race` | Race + sprint |
| `race+qual` | Race, sprint, qualifying, sprint qualifying — **default** |
| `all` | Every session including practice |

Sessions outside the configured set are still available — they're just processed on demand (with the usual 1–3 minute first-load wait) rather than pre-fetched.

### Manual setup (without Docker)

Expand Down
24 changes: 14 additions & 10 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from auth import is_auth_enabled, verify_token
from routers import sessions, track, laps, results, replay, telemetry, sync, live, live_status
from routers import auth_routes
from services.auto_precompute import auto_precompute_loop
from services.auto_precompute import auto_precompute_loop, get_allowed_session_types

load_dotenv()

Expand All @@ -22,16 +22,20 @@

@asynccontextmanager
async def lifespan(app: FastAPI):
# Start background auto-precompute task
task = asyncio.create_task(auto_precompute_loop())
logger.info("Auto-precompute background task scheduled")
allowed = get_allowed_session_types()
task: asyncio.Task | None = None
if allowed:
task = asyncio.create_task(auto_precompute_loop())
logger.info(f"Auto-precompute scheduled for session types: {sorted(allowed)}")
else:
logger.info("Auto-precompute disabled (AUTO_PRECOMPUTE=off)")
yield
# Cancel on shutdown
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass


app = FastAPI(
Expand Down
42 changes: 42 additions & 0 deletions backend/services/auto_precompute.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
Runs every 30 minutes on Friday–Monday (race weekend days).
Uses FastF1's schedule to find sessions that should have data available,
checks if we've already processed them, and runs precompute if not.

Which session types are auto-fetched is controlled by the AUTO_PRECOMPUTE
env var. See get_allowed_session_types() for accepted values.
"""

import asyncio
import logging
import os
import traceback
from datetime import datetime, timedelta, timezone

Expand All @@ -21,6 +25,37 @@
# How long after a session's scheduled start before we try to fetch data
DATA_AVAILABILITY_DELAY = timedelta(hours=0)

# AUTO_PRECOMPUTE presets: env value -> set of session type codes to fetch.
# Codes match SESSION_NAME_TO_TYPE in services/f1_data.py.
_AUTO_PRECOMPUTE_PRESETS: dict[str, set[str]] = {
"off": set(),
"race": {"R", "S"},
"race+qual": {"R", "S", "Q", "SQ"},
"all": {"R", "S", "Q", "SQ", "FP1", "FP2", "FP3"},
}
_AUTO_PRECOMPUTE_DEFAULT = "race+qual"


def get_allowed_session_types() -> set[str]:
"""Return the set of session type codes that should be auto-precomputed.

Driven by the AUTO_PRECOMPUTE env var. Accepted values:
- off : disable auto-precompute entirely
- race : race + sprint
- race+qual : race, sprint, qualifying, sprint qualifying (default)
- all : every session including practice

Unknown values fall back to the default with a warning.
"""
value = os.environ.get("AUTO_PRECOMPUTE", _AUTO_PRECOMPUTE_DEFAULT).strip().lower()
if value not in _AUTO_PRECOMPUTE_PRESETS:
logger.warning(
f"Unknown AUTO_PRECOMPUTE value '{value}', falling back to '{_AUTO_PRECOMPUTE_DEFAULT}'. "
f"Valid values: {sorted(_AUTO_PRECOMPUTE_PRESETS)}"
)
value = _AUTO_PRECOMPUTE_DEFAULT
return set(_AUTO_PRECOMPUTE_PRESETS[value])


async def _check_and_process():
"""Check for new sessions and process any that have data available."""
Expand All @@ -32,6 +67,10 @@ async def _check_and_process():
now = datetime.now(timezone.utc)
year = now.year

allowed_types = get_allowed_session_types()
if not allowed_types:
return

try:
events = _fetch_schedule_sync(year)
except Exception as e:
Expand All @@ -53,6 +92,9 @@ async def _check_and_process():
if not session_type:
continue

if session_type not in allowed_types:
continue

# Skip if session hasn't had enough time for data to be available
if now < session_ts + DATA_AVAILABILITY_DELAY:
continue
Expand Down
Loading