import time import json import uuid import os from fastapi import APIRouter, HTTPException, Depends, Header from pydantic import BaseModel from typing import Optional, Any, Dict from jsonschema import validate, ValidationError from app.models.user import get_db_connection from app.api.v1.auth import get_current_user, decode_token router = APIRouter() SCHEMA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "models", "project_schema.json") def upgrade_project_json_if_needed(project_data: dict) -> dict: if "main_session" in project_data: return project_data tracks = project_data.get("tracks", []) upgraded_tracks = [] for t in tracks: track_id = str(t.get("id", "")) track_name = t.get("name", "Track") vol = t.get("volumeDb", 0.0) pan = t.get("pan", 0.0) muted = t.get("muted", False) solo = t.get("solo", False) items = [] for c in t.get("clips", []): items.append({ "id": c.get("id"), "name": c.get("name", "Audio Clip"), "type": "AUDIO_ITEM", "start_bar": c.get("startTime", 0.0) / 4.0, "duration_bars": 4.0, "clip_start_offset_bars": 0.0, "source_data": { "audio_file_url": f"/static/audio/uploads/{t.get('serverFileId')}" if t.get("serverFileId") else "", "sample_rate": 44100, "channels": 2, "gain": 1.0 } }) for m in t.get("midiItems", []): items.append({ "id": m.get("id"), "name": m.get("name", "MIDI Item"), "type": "MIDI_ITEM", "start_bar": m.get("startTime", 0.0) / 4.0, "duration_bars": m.get("duration", 4.0), "clip_start_offset_bars": 0.0, "source_data": { "total_buffer_bars": m.get("duration", 8.0), "notes": m.get("notes", []) } }) upgraded_tracks.append({ "id": track_id, "name": track_name, "type": "MIDI" if t.get("midiItems") else "AUDIO", "volume_db": vol, "pan": pan, "mute": muted, "solo": solo, "fx_chain": [], "synth_engine": { "plugin_id": "synth", "preset_id": "default", "parameters": {} }, "items": items }) return { "project_id": project_data.get("id", "temp_project"), "metadata": { "title": project_data.get("name", "Dự án mới"), "bpm": 120.0, "time_signature_numerator": 4, "time_signature_denominator": 4, "sample_rate": 44100 }, "main_session": { "id": "main", "name": "MAIN SESSION", "is_root": True, "length_bars": 16.0, "auto_compute_length": True, "tracks": upgraded_tracks }, "section_store": {} } def validate_project_data(data_json: str) -> str: try: data = json.loads(data_json) if "main_session" not in data: data = upgrade_project_json_if_needed(data) data_json = json.dumps(data) if os.path.exists(SCHEMA_PATH): with open(SCHEMA_PATH, "r") as f: schema = json.load(f) validate(instance=data, schema=schema) return data_json except json.JSONDecodeError as e: raise HTTPException(status_code=400, detail=f"Cấu trúc JSON không hợp lệ: {str(e)}") except ValidationError as e: path = " -> ".join(str(p) for p in e.path) raise HTTPException(status_code=400, detail=f"Lỗi xác thực project schema tại [{path}]: {e.message}") except Exception as e: raise HTTPException(status_code=400, detail=f"Lỗi xác thực dự án: {str(e)}") class SaveProjectRequest(BaseModel): name: str data_json: str class SaveTempProjectRequest(BaseModel): data_json: str def get_optional_user(authorization: Optional[str] = Header(None)) -> Optional[dict]: if authorization and authorization.startswith("Bearer "): token = authorization.split(" ")[1] return decode_token(token) return None @router.post("/temp") async def save_temp_project(req: SaveTempProjectRequest, current_user: Optional[dict] = Depends(get_optional_user)): validated_data_json = validate_project_data(req.data_json) user_id = current_user["user_id"] if current_user else "anonymous" conn = get_db_connection() cursor = conn.cursor() size_bytes = len(validated_data_json.encode("utf-8")) now = time.time() temp_id = f"temp_{user_id}" cursor.execute(""" INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at) VALUES (?, ?, 'Dự án tạm chưa lưu', ?, 1, ?, ?) ON CONFLICT(id) DO UPDATE SET data_json = excluded.data_json, size_bytes = excluded.size_bytes, updated_at = excluded.updated_at """, (temp_id, user_id, validated_data_json, size_bytes, now)) conn.commit() conn.close() return {"message": "Đã lưu dự án tạm tự động", "updated_at": now} @router.get("/temp") async def get_temp_project(current_user: Optional[dict] = Depends(get_optional_user)): user_id = current_user["user_id"] if current_user else "anonymous" temp_id = f"temp_{user_id}" conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT data_json, updated_at FROM projects WHERE id = ? AND is_temp = 1", (temp_id,)) row = cursor.fetchone() conn.close() if not row: return {"has_temp": False} return { "has_temp": True, "data_json": row["data_json"], "updated_at": row["updated_at"] } @router.post("/cloud") async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depends(get_current_user)): validated_data_json = validate_project_data(req.data_json) user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT storage_limit_mb FROM user_quotas WHERE user_id = ?", (user_id,)) quota_row = cursor.fetchone() storage_limit_mb = quota_row["storage_limit_mb"] if quota_row else 500 cursor.execute("SELECT SUM(size_bytes) as total_used FROM projects WHERE user_id = ? AND is_temp = 0", (user_id,)) used_row = cursor.fetchone() used_bytes = used_row["total_used"] if used_row and used_row["total_used"] else 0 new_size_bytes = len(validated_data_json.encode("utf-8")) max_bytes = storage_limit_mb * 1024 * 1024 if used_bytes + new_size_bytes > max_bytes: conn.close() raise HTTPException( status_code=400, detail=f"Dung lượng dự án vượt quá hạn mức Quota ({storage_limit_mb}MB). Vui lòng dọn dẹp hoặc nâng cấp tài khoản." ) project_id = str(uuid.uuid4()) now = time.time() cursor.execute(""" INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?) """, (project_id, user_id, req.name, validated_data_json, new_size_bytes, now)) temp_id = f"temp_{user_id}" cursor.execute("DELETE FROM projects WHERE id = ? AND is_temp = 1", (temp_id,)) conn.commit() conn.close() return { "message": "Đã lưu dự án lên Cloud thành công!", "project_id": project_id } @router.get("/cloud") async def list_cloud_projects(current_user: dict = Depends(get_current_user)): user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute(""" SELECT id, name, size_bytes, updated_at FROM projects WHERE user_id = ? AND is_temp = 0 ORDER BY updated_at DESC """, (user_id,)) rows = cursor.fetchall() conn.close() return [ { "id": r["id"], "name": r["name"], "size_mb": round(r["size_bytes"] / (1024 * 1024), 2), "updated_at": r["updated_at"] } for r in rows ] @router.get("/cloud/{project_id}") async def get_cloud_project(project_id: str, current_user: dict = Depends(get_current_user)): user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT name, data_json FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id)) row = cursor.fetchone() conn.close() if not row: raise HTTPException(status_code=404, detail="Không tìm thấy dự án") return { "id": project_id, "name": row["name"], "data_json": row["data_json"] } @router.delete("/cloud/{project_id}") async def delete_cloud_project(project_id: str, current_user: dict = Depends(get_current_user)): user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute("DELETE FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id)) conn.commit() conn.close() return {"success": True, "message": "Đã xóa dự án thành công"} @router.put("/cloud/{project_id}") async def update_cloud_project(project_id: str, req: SaveProjectRequest, current_user: dict = Depends(get_current_user)): validated_data_json = validate_project_data(req.data_json) user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT id FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id)) exists = cursor.fetchone() if not exists: conn.close() raise HTTPException(status_code=404, detail="Không tìm thấy dự án để cập nhật") new_size_bytes = len(validated_data_json.encode("utf-8")) now = time.time() cursor.execute(""" UPDATE projects SET name = ?, data_json = ?, size_bytes = ?, updated_at = ? WHERE id = ? AND user_id = ? """, (req.name, validated_data_json, new_size_bytes, now, project_id, user_id)) conn.commit() conn.close() return {"success": True, "message": "Đã cập nhật dự án thành công"} class RenderProjectRequest(BaseModel): sample_rate: Optional[int] = 44100 @router.post("/cloud/{project_id}/render") async def render_project_endpoint(project_id: str, req: RenderProjectRequest, current_user: dict = Depends(get_current_user)): user_id = current_user["user_id"] conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT name, data_json FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id)) row = cursor.fetchone() conn.close() if not row: raise HTTPException(status_code=404, detail="Không tìm thấy dự án để kết xuất") # Validate schema validate_project_data(row["data_json"]) # Trigger Celery task from app.tasks.worker import render_project_task task = render_project_task.delay( project_id=project_id, project_name=row["name"], project_json_str=row["data_json"], sample_rate=req.sample_rate or 44100 ) return { "task_id": task.id, "status": "processing" }