F1 circuit layouts with year-by-year SVGs — manually traced track variations
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

205 行
8.1KB

  1. from fastapi import FastAPI, HTTPException, Response
  2. from fastapi.responses import FileResponse, Response as FastAPIResponse
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from pathlib import Path
  5. import logging
  6. # Import our CircuitService
  7. from circuits_service import CircuitService, Format
  8. from ergast_service import ErgastService
  9. from posters_service import PostersService
  10. from models.track_layout import TrackLayout
  11. # Initialize FastAPI app
  12. app = FastAPI(
  13. title="F1 Track Layouts API",
  14. description="API serving F1 track layout images, vectors and GeoJSON files",
  15. version="1.0.0"
  16. )
  17. # Add CORS middleware
  18. app.add_middleware(
  19. CORSMiddleware,
  20. allow_origins=["*"],
  21. allow_credentials=True,
  22. allow_methods=["*"],
  23. allow_headers=["*"],
  24. )
  25. # Configure logging
  26. logging.basicConfig(level=logging.INFO)
  27. logger = logging.getLogger(__name__)
  28. # Initialize CircuitService
  29. circuit_service = CircuitService()
  30. ergast_service = ErgastService(circuit_service)
  31. posters_service = PostersService(circuit_service, ergast_service)
  32. def get_content_disposition(format: Format) -> str:
  33. if format == Format.PNG:
  34. return "inline"
  35. # SVG and GeoJSON files as attachments for security/convention
  36. return "attachment"
  37. def get_media_type(format: Format) -> str:
  38. media_types = {
  39. Format.PNG: "image/png",
  40. Format.SVG: "image/svg+xml",
  41. Format.GEOJSON: "application/geo+json"
  42. }
  43. return media_types.get(format, "application/octet-stream") # fallback for unknown formats
  44. @app.get("/")
  45. async def root():
  46. """Root endpoint returning API information"""
  47. return {
  48. "name": "F1 Track Layouts API",
  49. "version": "1.0.0",
  50. "endpoints": [
  51. "/circuits",
  52. "/circuits/{country_slug}",
  53. "/circuits/{country_slug}/{city_slug}",
  54. "/circuits/{country_slug}/{city_slug}/{circuit_slug}",
  55. "/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}",
  56. "/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{format}",
  57. "/tracks/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{format}",
  58. "/grand-prix/{grand_prix_name}/year/{year}",
  59. "/posters/race/{grand_prix_name}/{season}.png",
  60. "/posters/season/{season}.png",
  61. ]
  62. }
  63. @app.get("/posters/race/{grand_prix_name}/{season}.png")
  64. async def get_race_poster(grand_prix_name: str, season: int):
  65. """Plex-ready 1000x1500 race poster (PNG). Cached on disk."""
  66. png = posters_service.render_race_poster(grand_prix_name, season)
  67. if not png:
  68. raise HTTPException(status_code=404, detail=f"No poster available for {grand_prix_name} {season}")
  69. return FastAPIResponse(
  70. content=png,
  71. media_type="image/png",
  72. headers={"Cache-Control": "public, max-age=31536000"},
  73. )
  74. @app.get("/posters/season/{season}.png")
  75. async def get_season_poster(season: int):
  76. """Plex-ready 1000x1500 season poster (PNG). Cached on disk."""
  77. png = posters_service.render_season_poster(season)
  78. return FastAPIResponse(
  79. content=png,
  80. media_type="image/png",
  81. headers={"Cache-Control": "public, max-age=31536000"},
  82. )
  83. @app.get("/circuits")
  84. async def get_countries():
  85. """Get list of all available countries with their slugs"""
  86. return {
  87. "countries": [c.to_dict() for c in circuit_service.get_countries_list()]
  88. }
  89. @app.get("/circuits/{country_slug}")
  90. async def get_cities(country_slug: str):
  91. """Get list of cities for a specific country"""
  92. cities = circuit_service.get_localities_list(country_slug)
  93. if not cities:
  94. raise HTTPException(status_code=404, detail="Country not found")
  95. return {"cities": [c.to_dict() for c in cities]}
  96. @app.get("/circuits/{country_slug}/{city_slug}")
  97. async def get_circuits(country_slug: str, city_slug: str):
  98. """Get list of circuits for a specific city"""
  99. circuits = circuit_service.get_circuits_list(country_slug, city_slug)
  100. if not circuits:
  101. raise HTTPException(status_code=404, detail="City not found")
  102. return {"circuits": [c.to_dict() for c in circuits]}
  103. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}")
  104. async def get_circuit_details(country_slug: str, city_slug: str, circuit_slug: str):
  105. """Get circuit details and available formats"""
  106. circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
  107. if not circuit_details:
  108. raise HTTPException(status_code=404, detail="Circuit not found")
  109. return circuit_details.to_dict()
  110. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}")
  111. async def get_layout_details(country_slug: str, city_slug: str, circuit_slug: str, layout_slug: str):
  112. """Get details for a specific layout"""
  113. layout_details = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
  114. if not layout_details:
  115. raise HTTPException(status_code=404, detail="Layout not found")
  116. return layout_details.to_dict()
  117. @app.get("/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{image_format}")
  118. async def get_track_by_year(
  119. country_slug: str,
  120. city_slug: str,
  121. circuit_slug: str,
  122. year: int,
  123. image_format: Format,
  124. response: Response
  125. ):
  126. """Get track layout for a specific year"""
  127. circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
  128. if not circuit_details:
  129. raise HTTPException(status_code=404, detail="Circuit not found")
  130. try:
  131. layout_slug = circuit_service.find_layout_slug_for_year(circuit_details, year)
  132. return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
  133. except KeyError as e:
  134. logger.error(f"KeyError in get_track_by_year: {str(e)}")
  135. raise HTTPException(status_code=404, detail="Circuit or layout not found")
  136. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{image_format}")
  137. async def get_track_by_layout(
  138. country_slug: str,
  139. city_slug: str,
  140. circuit_slug: str,
  141. layout_slug: str,
  142. image_format: Format,
  143. response: Response
  144. ):
  145. """Get specific track layout"""
  146. return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
  147. @app.get("/grand-prix/{grand_prix_name}/{season}/{image_format}")
  148. async def get_grand_prix_circuit(grand_prix_name: str, season: int, image_format: Format, response: Response):
  149. layout = circuit_service.get_circuit_layout_by_ergast_data(grand_prix_name, season)
  150. if not layout:
  151. raise HTTPException(status_code=404, detail="Circuit not found")
  152. try:
  153. return get_file_response(layout, image_format, response)
  154. except KeyError as e:
  155. logger.error(f"KeyError in get_track_by_year: {str(e)}")
  156. raise HTTPException(status_code=404, detail="Circuit or layout not found")
  157. def get_file_response(layout: TrackLayout, image_format: Format, response: Response) -> FileResponse:
  158. # layout = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
  159. file_path = Path(f"./circuits").joinpath(layout.relative_svg_filepath)
  160. if not file_path or not file_path.exists():
  161. logger.error(f"File not found: {file_path}")
  162. logger.info(f"Looking for {layout.slug} in format {image_format}")
  163. raise HTTPException(status_code=404, detail=f"File not found in {image_format} format")
  164. # Get actual filename from the path for the Content-Disposition header
  165. filename = file_path.name
  166. logger.info(f"Serving file: {file_path}")
  167. return FileResponse(
  168. file_path,
  169. media_type=get_media_type(image_format),
  170. content_disposition_type=get_content_disposition(image_format),
  171. filename=filename,
  172. headers={
  173. "Cache-Control": "public, max-age=31536000",
  174. "ETag": f"{layout.circuit.locality.country.slug}/{layout.circuit.locality.slug}/{layout.circuit.slug}-{layout.slug}-{image_format}"
  175. }
  176. )
  177. if __name__ == "__main__":
  178. import uvicorn
  179. uvicorn.run(app, host="0.0.0.0", port=8000)