main.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. from fastapi import FastAPI, WebSocket, WebSocketDisconnect
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from fastapi.responses import FileResponse
  5. import asyncio
  6. import logging
  7. import os
  8. import json
  9. import pymysql
  10. from backend.app.core.config import settings
  11. from backend.app.core.database import engine, SessionLocal
  12. from backend.app.models import sql_models
  13. from backend.app.api.api import api_router
  14. from backend.app.services.video_core import video_manager
  15. from backend.app.services.scheduler import init_scheduler
  16. from backend.app.core import security
  17. # Setup logging
  18. logging.basicConfig(level=logging.INFO)
  19. logger = logging.getLogger(__name__)
  20. app = FastAPI(title=settings.PROJECT_NAME)
  21. # CORS
  22. app.add_middleware(
  23. CORSMiddleware,
  24. allow_origins=["*"],
  25. allow_credentials=True,
  26. allow_methods=["*"],
  27. allow_headers=["*"],
  28. )
  29. # API Routes
  30. app.include_router(api_router, prefix=settings.API_V1_STR)
  31. # Static Files
  32. # Ensure directories exist
  33. os.makedirs("backend/app/static", exist_ok=True)
  34. os.makedirs("backend/dist", exist_ok=True)
  35. os.makedirs("backend/dist/assets", exist_ok=True)
  36. app.mount("/static", StaticFiles(directory="backend/app/static"), name="static")
  37. def init_db_if_not_exists():
  38. """Check if database exists, create if not."""
  39. max_retries = 30
  40. retry_interval = 2 # seconds
  41. for attempt in range(max_retries):
  42. try:
  43. logger.info(f"Connecting to MySQL server at {settings.MYSQL_SERVER} to check database (Attempt {attempt + 1}/{max_retries})...")
  44. conn = pymysql.connect(
  45. host=settings.MYSQL_SERVER,
  46. user=settings.MYSQL_USER,
  47. password=settings.MYSQL_PASSWORD,
  48. port=int(settings.MYSQL_PORT),
  49. charset='utf8mb4',
  50. cursorclass=pymysql.cursors.DictCursor
  51. )
  52. with conn.cursor() as cursor:
  53. logger.info(f"Checking database {settings.MYSQL_DB}...")
  54. cursor.execute(f"CREATE DATABASE IF NOT EXISTS {settings.MYSQL_DB} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")
  55. logger.info("Database checked/created.")
  56. conn.close()
  57. return # Success
  58. except pymysql.err.OperationalError as e:
  59. if e.args[0] == 2003: # Connection refused
  60. logger.warning(f"Database not ready yet, retrying in {retry_interval}s...")
  61. import time
  62. time.sleep(retry_interval)
  63. else:
  64. logger.error(f"Database initialization failed: {e}")
  65. # Don't raise immediately, maybe transient? But usually logic error.
  66. # Let's retry anyway to be safe, or break if critical.
  67. time.sleep(retry_interval)
  68. except Exception as e:
  69. logger.error(f"Database initialization failed: {e}")
  70. import time
  71. time.sleep(retry_interval)
  72. logger.error("Could not connect to database after multiple attempts.")
  73. # raise Exception("Database connection failed") # Optional: crash app
  74. # Startup Event
  75. @app.on_event("startup")
  76. async def startup_event():
  77. logger.info("Starting up...")
  78. # 1. Ensure Database Exists (from init_db_script.py)
  79. init_db_if_not_exists()
  80. # 2. Ensure Tables Exist
  81. try:
  82. sql_models.Base.metadata.create_all(bind=engine)
  83. logger.info("Tables created/verified.")
  84. except Exception as e:
  85. logger.error(f"Error creating tables: {e}")
  86. # 3. Init Admin User (from reset_admin.py)
  87. db = SessionLocal()
  88. try:
  89. username = "admin"
  90. password = "HNYZ0821"
  91. admin = db.query(sql_models.User).filter(sql_models.User.username == username).first()
  92. if admin:
  93. logger.info(f"User '{username}' found. Resetting password...")
  94. admin.hashed_password = security.get_password_hash(password)
  95. admin.is_active = True
  96. db.commit()
  97. logger.info(f"Password for '{username}' has been reset.")
  98. else:
  99. logger.info(f"User '{username}' not found. Creating...")
  100. hashed_pwd = security.get_password_hash(password)
  101. user = sql_models.User(username=username, hashed_password=hashed_pwd, is_active=True)
  102. db.add(user)
  103. db.commit()
  104. logger.info(f"User '{username}' created.")
  105. except Exception as e:
  106. logger.error(f"Error initializing admin user: {e}")
  107. # Init Video Streams
  108. try:
  109. cameras = db.query(sql_models.Camera).all()
  110. for cam in cameras:
  111. if cam.stream_url:
  112. video_manager.add_stream(cam.id, cam.stream_url)
  113. except Exception as e:
  114. logger.error(f"Error loading cameras: {e}")
  115. db.close()
  116. # Init Scheduler
  117. init_scheduler()
  118. # WebSocket
  119. @app.websocket("/ws/stream")
  120. async def websocket_endpoint(websocket: WebSocket):
  121. await websocket.accept()
  122. try:
  123. while True:
  124. # 1 FPS
  125. await asyncio.sleep(1)
  126. snapshot_data = video_manager.get_all_snapshots_base64()
  127. await websocket.send_text(json.dumps(snapshot_data))
  128. except WebSocketDisconnect:
  129. logger.info("WebSocket disconnected")
  130. except Exception as e:
  131. logger.error(f"WebSocket error: {e}")
  132. # Frontend Static (Production)
  133. app.mount("/assets", StaticFiles(directory="backend/dist/assets"), name="assets")
  134. @app.get("/{full_path:path}")
  135. async def catch_all(full_path: str):
  136. # API requests are handled above.
  137. # This handles frontend routing.
  138. dist_path = "backend/dist"
  139. file_path = os.path.join(dist_path, full_path)
  140. if os.path.exists(file_path) and os.path.isfile(file_path):
  141. return FileResponse(file_path)
  142. # Default to index.html for SPA
  143. index_path = os.path.join(dist_path, "index.html")
  144. if os.path.exists(index_path):
  145. return FileResponse(index_path)
  146. return FileResponse(file_path) # Fallback (might 404)