Spaces:
Running
Running
| 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() | |
| async def startup_event(): | |
| import asyncio | |
| manager.loop = asyncio.get_running_loop() | |
| print("[WebSocket] Event loop registered in connection manager.") | |
| 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) | |
| def read_root(): | |
| return {"message": "Melodix API is running"} | |
| 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)} | |