from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles from app.database import engine, Base from app.routers import auth, songs, lyrics, iglesia, aprendizaje, midi, alabanza, workers, admin, migration from app.models.user import User, AdminUser, AdminNotification, LandingGalleryItem from app.models.process import Task, Worker from app.models.lyrics import Lyric, Repertorio, Group, UserGroup, Invitation from app.models.iglesia import Iglesia, IglesiaMember, IglesiaRol, Calendario, Servicio, Asistencia, Finanza, Momento, RelacionFamiliar, Inventario, Miembro, DismissedVisita from app.models.sync import B2SyncQueue, MigrationBackup import app.models.aprendizaje # Registrar modelos de aprendizaje import app.models.process # Asegura que las tablas de tareas se registren en Base import app.models.midi # Registrar modelos de partituras/MIDI import os # Crear tablas (en producción usar Alembic) Base.metadata.create_all(bind=engine) # Migración en caliente: añadir columnas que pueden no existir en BDs antiguas _migrations = [ # users "ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR", # tasks "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS chords TEXT", # b2_sync_queue "ALTER TABLE b2_sync_queue ADD COLUMN IF NOT EXISTS is_uploaded BOOLEAN DEFAULT TRUE", "ALTER TABLE b2_sync_queue ADD COLUMN IF NOT EXISTS uploading_by TEXT DEFAULT NULL", "ALTER TABLE b2_sync_queue ADD COLUMN IF NOT EXISTS uploading_claimed_at TIMESTAMP DEFAULT NULL", "ALTER TABLE b2_sync_queue ADD COLUMN IF NOT EXISTS missing_stems JSONB DEFAULT NULL", # lyrics "ALTER TABLE lyrics ADD COLUMN IF NOT EXISTS transpose_offset INTEGER DEFAULT 0", # dismissed_visitas (creada en caliente si no existe) """ CREATE TABLE IF NOT EXISTS dismissed_visitas ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), miembro_id INTEGER REFERENCES miembros(id), dismissed_at TIMESTAMP DEFAULT NOW() ) """, # finanzas "ALTER TABLE finanzas ADD COLUMN IF NOT EXISTS reference_uuid TEXT", # workers "ALTER TABLE workers ADD COLUMN IF NOT EXISTS user_id INTEGER", "ALTER TABLE workers ADD COLUMN IF NOT EXISTS token VARCHAR", "ALTER TABLE workers ADD COLUMN IF NOT EXISTS command VARCHAR DEFAULT 'none'", "ALTER TABLE workers ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT FALSE", # torneos "ALTER TABLE torneos ADD COLUMN IF NOT EXISTS primero_termino_en VARCHAR", # tasks — bpm y beats "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS bpm FLOAT", "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS beats TEXT", ] from sqlalchemy import text as _sql_text for _mig_sql in _migrations: try: with engine.begin() as _conn: _conn.execute(_sql_text(_mig_sql)) except Exception as _mig_err: pass # Columna ya existe u otro error no crítico # Sembrar usuario Administrador por defecto si no existe from app.database import SessionLocal from app.routers.auth import get_password_hash db = SessionLocal() try: admin_exists = db.query(AdminUser).filter(AdminUser.username == "isaacsalgado105@gmail.com").first() if not admin_exists: admin_pass = os.getenv("DEFAULT_ADMIN_PASSWORD", "Dis147258369") hashed = get_password_hash(admin_pass) admin_user = AdminUser( username="isaacsalgado105@gmail.com", hashed_password=hashed, perm_users=True, perm_workers=True, perm_churches=True, perm_groups=True, perm_notifications=True ) db.add(admin_user) db.commit() # Generar una notificación inicial de bienvenida initial_notif = AdminNotification( title="Bienvenido a Melodix Admin", message="El panel de control administrativo y sistema de monitoreo ha sido configurado.", read=False ) db.add(initial_notif) db.commit() print("Usuario Administrador por defecto sembrado exitosamente.") else: # Migración en caliente: si las columnas de permisos son NULL, asignar True changed = False for perm_col in ('perm_users', 'perm_workers', 'perm_churches', 'perm_groups', 'perm_notifications'): if getattr(admin_exists, perm_col, None) is None: setattr(admin_exists, perm_col, True) changed = True if changed: db.commit() print("Permisos del administrador actualizados a True (migración en caliente).") except Exception as e: print(f"Error sembrando usuario administrador: {e}") finally: db.close() from fastapi import FastAPI, WebSocket, WebSocketDisconnect from app.websocket_manager import manager app = FastAPI() @app.on_event("startup") async def startup_event(): import asyncio manager.loop = asyncio.get_running_loop() print("[WebSocket] Event loop registered in connection manager.") @app.websocket("/ws/{user_id}") async def websocket_endpoint(websocket: WebSocket, user_id: int): await manager.connect(user_id, websocket) try: while True: # Mantener la conexión abierta recibiendo datos dummy await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(user_id) # Configuración CORS allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "*") if allowed_origins_env and allowed_origins_env != "*": origins = [origin.strip() for origin in allowed_origins_env.split(",") if origin.strip()] else: origins = ["*"] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(GZipMiddleware, minimum_size=500) # Servir archivos estáticos (audios) # Ajustar rutas relativas si es necesario BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROCESSED_DIR = os.path.join(BASE_DIR, "processed") os.makedirs(PROCESSED_DIR, exist_ok=True) app.mount("/audios", StaticFiles(directory=PROCESSED_DIR), name="audios") # Incluir routers app.include_router(auth.router) app.include_router(songs.router) app.include_router(lyrics.router) app.include_router(iglesia.router) app.include_router(aprendizaje.router) app.include_router(midi.router) app.include_router(alabanza.router) app.include_router(workers.router) app.include_router(admin.router) app.include_router(migration.router) @app.get("/") def read_root(): return {"message": "Melodix API is running"} @app.get("/health") def health_check(): """Diagnóstico de conectividad con la base de datos y estado de tablas.""" import os from sqlalchemy import text, inspect from app.database import engine try: with engine.connect() as conn: conn.execute(text("SELECT 1")) inspector = inspect(engine) tables = inspector.get_table_names() return { "status": "ok", "database": os.getenv("DATABASE_URL", "SQLite (local)").split("@")[-1] if os.getenv("DATABASE_URL") else "SQLite (local)", "tables_count": len(tables), "b2_sync_queue_exists": "b2_sync_queue" in tables, "users_exists": "users" in tables, "devices_exists": "devices" in tables, "tables": tables } except Exception as e: return {"status": "error", "detail": str(e)}