GET /v1/position?session_key=9158&date>2024-05-18T13:00:00 GET /v1/intervals?session_key=9158&date>2024-05-18T13:00:00 GET /v1/laps?session_key=9158 GET /v1/stints?session_key=9158 GET /v1/pit?session_key=9158 GET /v1/race_control?session_key=9158 GET /v1/team_radio?session_key=9158 GET /v1/weather?session_key=9158 CODE_BLOCK: GET /v1/position?session_key=9158&date>2024-05-18T13:00:00 GET /v1/intervals?session_key=9158&date>2024-05-18T13:00:00 GET /v1/laps?session_key=9158 GET /v1/stints?session_key=9158 GET /v1/pit?session_key=9158 GET /v1/race_control?session_key=9158 GET /v1/team_radio?session_key=9158 GET /v1/weather?session_key=9158 CODE_BLOCK: GET /v1/position?session_key=9158&date>2024-05-18T13:00:00 GET /v1/intervals?session_key=9158&date>2024-05-18T13:00:00 GET /v1/laps?session_key=9158 GET /v1/stints?session_key=9158 GET /v1/pit?session_key=9158 GET /v1/race_control?session_key=9158 GET /v1/team_radio?session_key=9158 GET /v1/weather?session_key=9158 COMMAND_BLOCK: url = f"https://api.jolpi.ca/ergast/f1/{SEASON}/driverStandings.json" data = requests.get(url).json() lists = data["MRData"]["StandingsTable"]["StandingsLists"] for ds in lists[0]["DriverStandings"]: team = ds["Constructors"][0]["name"] # plural array, not Constructor.name driver_code = ds["Driver"]["code"] points = ds["points"] COMMAND_BLOCK: url = f"https://api.jolpi.ca/ergast/f1/{SEASON}/driverStandings.json" data = requests.get(url).json() lists = data["MRData"]["StandingsTable"]["StandingsLists"] for ds in lists[0]["DriverStandings"]: team = ds["Constructors"][0]["name"] # plural array, not Constructor.name driver_code = ds["Driver"]["code"] points = ds["points"] COMMAND_BLOCK: url = f"https://api.jolpi.ca/ergast/f1/{SEASON}/driverStandings.json" data = requests.get(url).json() lists = data["MRData"]["StandingsTable"]["StandingsLists"] for ds in lists[0]["DriverStandings"]: team = ds["Constructors"][0]["name"] # plural array, not Constructor.name driver_code = ds["Driver"]["code"] points = ds["points"] CODE_BLOCK: f1.jsx (refreshes every 3 seconds) └── shell: python3 ~/.f1/engine.py └── fetch: http://127.0.0.1:9877/api/session ← live standings + RC └── fetch: http://127.0.0.1:9877/api/radio ← team radio └── fetch: http://127.0.0.1:9877/api/schedule ← calendar CODE_BLOCK: f1.jsx (refreshes every 3 seconds) └── shell: python3 ~/.f1/engine.py └── fetch: http://127.0.0.1:9877/api/session ← live standings + RC └── fetch: http://127.0.0.1:9877/api/radio ← team radio └── fetch: http://127.0.0.1:9877/api/schedule ← calendar CODE_BLOCK: f1.jsx (refreshes every 3 seconds) └── shell: python3 ~/.f1/engine.py └── fetch: http://127.0.0.1:9877/api/session ← live standings + RC └── fetch: http://127.0.0.1:9877/api/radio ← team radio └── fetch: http://127.0.0.1:9877/api/schedule ← calendar COMMAND_BLOCK: def resolve_hls_audio(url): if ".m3u8" not in url.lower(): return url resp = requests.get(url, timeout=6) text = resp.text # Prefer explicit audio renditions audio = re.findall( r'#EXT-X-MEDIA:TYPE=AUDIO[^\n]*?URI="([^"]+)"', text ) for cand in audio: full = urljoin(url, cand) if "-b/" not in full and "-b." not in full: return full # Fall back to lowest-bandwidth video variant variants = [] for i, line in enumerate(text.splitlines()): if line.startswith("#EXT-X-STREAM-INF"): bw = int(re.search(r'BANDWIDTH=(\d+)', line).group(1)) variants.append((bw, urljoin(url, text.splitlines()[i+1]))) if variants: return sorted(variants)[0][1] return url COMMAND_BLOCK: def resolve_hls_audio(url): if ".m3u8" not in url.lower(): return url resp = requests.get(url, timeout=6) text = resp.text # Prefer explicit audio renditions audio = re.findall( r'#EXT-X-MEDIA:TYPE=AUDIO[^\n]*?URI="([^"]+)"', text ) for cand in audio: full = urljoin(url, cand) if "-b/" not in full and "-b." not in full: return full # Fall back to lowest-bandwidth video variant variants = [] for i, line in enumerate(text.splitlines()): if line.startswith("#EXT-X-STREAM-INF"): bw = int(re.search(r'BANDWIDTH=(\d+)', line).group(1)) variants.append((bw, urljoin(url, text.splitlines()[i+1]))) if variants: return sorted(variants)[0][1] return url COMMAND_BLOCK: def resolve_hls_audio(url): if ".m3u8" not in url.lower(): return url resp = requests.get(url, timeout=6) text = resp.text # Prefer explicit audio renditions audio = re.findall( r'#EXT-X-MEDIA:TYPE=AUDIO[^\n]*?URI="([^"]+)"', text ) for cand in audio: full = urljoin(url, cand) if "-b/" not in full and "-b." not in full: return full # Fall back to lowest-bandwidth video variant variants = [] for i, line in enumerate(text.splitlines()): if line.startswith("#EXT-X-STREAM-INF"): bw = int(re.search(r'BANDWIDTH=(\d+)', line).group(1)) variants.append((bw, urljoin(url, text.splitlines()[i+1]))) if variants: return sorted(variants)[0][1] return url
- Live standings — position, driver code, gap to leader, last lap time, tyre compound and age, pit stop count
- Race Control banner — Safety Car, Virtual Safety Car, Red Flag with colour-coded flashing overlay
- Side panel — Team radio recordings (playable), all RC messages, track weather
- Audio streams — ARD, BBC Radio 5, talkSPORT, ORF Sport Plus and more, played via mpv
- Date filters must be ISO 8601, not Unix timestamps: date>2024-05-18T13:00:00 ✅, date>1716033600 ❌ - rainfall is an integer (0 = dry, higher = wetter), not a boolean - gap_to_leader from /intervals is a float for normal gaps but a string like "+1 LAP" for lapped cars — always parseFloat() and guard before calling .toFixed() - compound from /stints is one of SOFT, MEDIUM, HARD, INTERMEDIATE, WET (uppercase strings)
- Response caching (hitting OpenF1 20 times per minute per endpoint is unnecessary)
- Background stream health checks - mpv process management via Unix socket
- State persistence between widget refresh cycles
- No ${VAR} in the command string — the backtick template literal is evaluated by the shell, and ${} crashes the widget. Use $(subshell) for shell substitutions.
- No child_process — use the built-in run() helper instead
- No // comments inside the bash heredoc — they cause parse errors - refreshFrequency is in milliseconds
- Sprint weekend detection (Sprint Shootout sessions have a different session name)
- Driver headshot images from OpenF1's headshot_url field
- Push notifications for race start / chequered flag via macOS Notification Center
- GitHub repo
- OpenF1 API docs
- Jolpica API