diff --git a/.env.example b/.env.example index aa750b6..cf8f3ea 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc4ee4..67bf2d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 86a4920..5683d92 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/backend/main.py b/backend/main.py index d49ab7f..a5fa4dc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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() @@ -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( diff --git a/backend/services/auto_precompute.py b/backend/services/auto_precompute.py index b8793a3..6746030 100644 --- a/backend/services/auto_precompute.py +++ b/backend/services/auto_precompute.py @@ -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 @@ -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.""" @@ -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: @@ -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