"""Main Quart application factory.""" import asyncio import logging from pathlib import Path from quart import Quart, send_from_directory, send_file from quart_cors import cors from app.config import get_settings from app.models import init_db from app.api import subscriptions, sources, downloads, credentials, settings as settings_api, platforms, websocket from app.api.websocket import start_redis_subscriber # Path to static frontend files (built Vue app) STATIC_DIR = Path(__file__).parent.parent / "static" def create_app() -> Quart: """Create and configure the Quart application.""" app = Quart(__name__, static_folder=None) # Disable default static handling config = get_settings() # Configure logging logging.basicConfig( level=getattr(logging, config.log_level.upper()), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) # Enable CORS for API routes only app = cors(app, allow_origin="*") # Register API blueprints app.register_blueprint(subscriptions.bp, url_prefix="/api/subscriptions") app.register_blueprint(sources.bp, url_prefix="/api/sources") app.register_blueprint(downloads.bp, url_prefix="/api/downloads") app.register_blueprint(credentials.bp, url_prefix="/api/credentials") app.register_blueprint(settings_api.bp, url_prefix="/api/settings") app.register_blueprint(platforms.bp, url_prefix="/api/platforms") app.register_blueprint(websocket.bp, url_prefix="/ws") @app.before_serving async def startup(): """Initialize database connection on startup.""" app.db_engine = await init_db(config.async_database_url) app.logger.info("Database connection initialized") app.logger.info(f"Static files directory: {STATIC_DIR} (exists: {STATIC_DIR.exists()})") # Start Redis subscriber for WebSocket events from Celery tasks app.redis_subscriber_task = asyncio.create_task(start_redis_subscriber()) app.logger.info("Redis subscriber started for WebSocket events") @app.after_serving async def shutdown(): """Close database connection on shutdown.""" # Cancel Redis subscriber task if hasattr(app, "redis_subscriber_task"): app.redis_subscriber_task.cancel() try: await app.redis_subscriber_task except asyncio.CancelledError: pass app.logger.info("Redis subscriber stopped") if hasattr(app, "db_engine"): await app.db_engine.dispose() app.logger.info("Database connection closed") @app.route("/health") async def health_check(): """Health check endpoint.""" return {"status": "healthy"} @app.route("/api") async def api_info(): """API information endpoint.""" return { "name": "Gallery Subscriber API", "version": "1.0.0", "endpoints": { "subscriptions": "/api/subscriptions", "sources": "/api/sources", "downloads": "/api/downloads", "credentials": "/api/credentials", "settings": "/api/settings", "platforms": "/api/platforms", "websocket": "/ws/events", }, } # Serve static assets (JS, CSS, images, fonts) @app.route("/assets/") async def serve_assets(filename): """Serve static assets from the frontend build.""" return await send_from_directory(STATIC_DIR / "assets", filename) # Serve other static files (favicon, etc.) @app.route("/") async def serve_static(filename): """Serve static files or fall back to index.html for Vue Router.""" file_path = STATIC_DIR / filename if file_path.exists() and file_path.is_file(): return await send_from_directory(STATIC_DIR, filename) # Fall back to index.html for Vue Router (SPA) return await send_file(STATIC_DIR / "index.html") # Serve index.html for root path @app.route("/") async def serve_index(): """Serve the frontend index.html.""" return await send_file(STATIC_DIR / "index.html") return app # Create app instance for hypercorn app = create_app()