Map backgrounds (aerial/osm/dark/light), bbox zoom, world locator, kometa YAML endpoint

posters_service:
- BackgroundStyle enum + tile stitcher for race posters
  (Esri aerial / OSM Carto / CartoDB dark / CartoDB light / none)
- _resolve_circuit_geo: GeoJSON bbox centre + auto-zoom (no hand tuning),
  falls back to Jolpica lat/lon + f1-locations.json zoom
- _draw_track_projected: project the layout's GeoJSON points into the
  same window as the tile background, so the white outline aligns with
  the actual track (the hand-drawn SVGs had arbitrary orientations)
- _render_world_locator: 140x140 CartoDB Dark inset at zoom 5 with a
  red marker pinning the circuit on the continent
- _race_dark_overlay: dimming so the heading + footer stay legible
  against any underlying tiles
- render_kometa_metadata: emits a Kometa-compatible YAML covering
  every season 1950..current+1 with per-show + per-round url_poster

cdn-api:
- GET /posters/race/{name}/{year}.png supports ?bg= query param
- GET /kometa/formula-1-metadata.yml serves the generated config so
  Kometa pulls it via metadata_files.url (auto-updates with schedule)
This commit is contained in:
2026-06-14 17:42:02 +02:00
parent 99ac83c706
commit 9430d499cc
2 changed files with 424 additions and 18 deletions
+33 -5
View File
@@ -1,5 +1,5 @@
from fastapi import FastAPI, HTTPException, Response
from fastapi.responses import FileResponse, Response as FastAPIResponse
from fastapi.responses import FileResponse, Response as FastAPIResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path
import logging
@@ -7,7 +7,7 @@ import logging
# Import our CircuitService
from circuits_service import CircuitService, Format
from ergast_service import ErgastService
from posters_service import PostersService
from posters_service import PostersService, BackgroundStyle
from models.track_layout import TrackLayout
# Initialize FastAPI app
@@ -71,9 +71,15 @@ async def root():
@app.get("/posters/race/{grand_prix_name}/{season}.png")
async def get_race_poster(grand_prix_name: str, season: int):
"""Plex-ready 1000x1500 race poster (PNG). Cached on disk."""
png = posters_service.render_race_poster(grand_prix_name, season)
async def get_race_poster(grand_prix_name: str, season: int, bg: BackgroundStyle = BackgroundStyle.AERIAL):
"""Plex-ready 1000x1500 race poster (PNG). Cached on disk per (race, bg).
`?bg=` selects the backdrop:
- aerial (default) — Esri World Imagery satellite at the circuit's coords
- osm — OpenStreetMap Carto street-map at the circuit's coords
- none — plain black background (skip tile fetch)
"""
png = posters_service.render_race_poster(grand_prix_name, season, bg=bg)
if not png:
raise HTTPException(status_code=404, detail=f"No poster available for {grand_prix_name} {season}")
return FastAPIResponse(
@@ -93,6 +99,28 @@ async def get_season_poster(season: int):
headers={"Cache-Control": "public, max-age=31536000"},
)
@app.get("/kometa/formula-1-metadata.yml")
async def get_kometa_metadata():
"""Generate a Kometa-compatible metadata YAML for the Formula 1 library.
One entry per F1 season from 1950..current+1. Each entry:
- uses Kometa's f1_season builder for round/episode names
- sets url_poster on the show (per-season hero from files-api)
- sets url_poster on each Plex 'season' (== each F1 round) from the
f1-circuits race poster endpoint
Kometa harmlessly skips shows / seasons that don't exist in the Plex
library, so we can emit the full schedule and let Kometa do the matching.
"""
yaml = posters_service.render_kometa_metadata()
return PlainTextResponse(
content=yaml,
media_type="text/yaml; charset=utf-8",
# YAML changes only when the Jolpica schedule changes — cache 1h.
headers={"Cache-Control": "public, max-age=3600"},
)
@app.get("/circuits")
async def get_countries():
"""Get list of all available countries with their slugs"""