From 8e7a3806c58d7a1d92dae9fa31096654e8f9e92c Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 1 May 2026 11:59:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(music-lab):=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=206=EA=B0=9C=20+=20?= =?UTF-8?q?=EC=88=98=EC=9D=B5=ED=99=94=205=EA=B0=9C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- music-lab/app/main.py | 143 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/music-lab/app/main.py b/music-lab/app/main.py index a3b057f..ddf8931 100644 --- a/music-lab/app/main.py +++ b/music-lab/app/main.py @@ -1,4 +1,6 @@ +import json import os +import shutil import uuid from typing import List, Optional from fastapi import FastAPI, HTTPException, BackgroundTasks @@ -11,6 +13,10 @@ from .db import ( get_all_tracks, add_track, delete_track, get_track_file_path, get_track_by_task_id, update_track_duration, update_track_file_info, update_track_hash, get_all_lyrics, add_lyrics, update_lyrics, delete_lyrics, + create_video_project, get_video_project, get_all_video_projects, + update_video_project_status, delete_video_project, + create_revenue_record, get_all_revenue_records, + update_revenue_record, delete_revenue_record, get_revenue_dashboard, ) from .local_provider import run_local_generation from .suno_provider import ( @@ -33,6 +39,7 @@ app.add_middleware( ) MUSIC_DATA_DIR = "/app/data" +VIDEO_DATA_DIR = os.getenv("VIDEO_DATA_DIR", "/app/data") def _get_mp3_duration(file_path: str) -> Optional[int]: @@ -669,3 +676,139 @@ def remove_lyrics(lyrics_id: int): if not delete_lyrics(lyrics_id): raise HTTPException(status_code=404, detail="Lyrics not found") return {"ok": True} + + +# ── 영상 프로젝트 모델 ──────────────────────────────────────────────────────── + +class VideoProjectCreate(BaseModel): + track_id: int + format: str = "visualizer" + target_countries: List[str] = [] + render_params: dict = {} + + +class RevenueCreate(BaseModel): + video_project_id: Optional[int] = None + yt_video_id: str = "" + record_month: str + views: int = 0 + watch_hours: float = 0.0 + revenue_usd: float = 0.0 + country: str = "" + source: str = "manual" + + +class RevenueUpdate(BaseModel): + yt_video_id: Optional[str] = None + record_month: Optional[str] = None + views: Optional[int] = None + watch_hours: Optional[float] = None + revenue_usd: Optional[float] = None + country: Optional[str] = None + source: Optional[str] = None + + +# ── 영상 프로젝트 API ───────────────────────────────────────────────────────── + +@app.post("/api/music/video-project", status_code=201) +def create_project(req: VideoProjectCreate, background_tasks: BackgroundTasks): + from .db import get_track_by_id + if not get_track_by_id(req.track_id): + raise HTTPException(status_code=404, detail="Track not found") + if req.format not in ("visualizer", "slideshow"): + raise HTTPException(status_code=400, detail="format은 'visualizer' 또는 'slideshow'") + proj = create_video_project(req.model_dump()) + return proj + + +@app.get("/api/music/video-projects") +def list_projects(): + return {"projects": get_all_video_projects()} + + +@app.get("/api/music/video-project/{project_id}") +def get_project(project_id: int): + proj = get_video_project(project_id) + if not proj: + raise HTTPException(status_code=404, detail="Project not found") + return proj + + +@app.post("/api/music/video-project/{project_id}/render") +def render_project(project_id: int, background_tasks: BackgroundTasks): + proj = get_video_project(project_id) + if not proj: + raise HTTPException(status_code=404, detail="Project not found") + if proj["status"] == "rendering": + raise HTTPException(status_code=409, detail="이미 렌더링 중입니다") + from .video_producer import produce_video + background_tasks.add_task(produce_video, project_id) + return {"ok": True, "project_id": project_id, "status": "rendering"} + + +@app.get("/api/music/video-project/{project_id}/export") +def export_project(project_id: int): + proj = get_video_project(project_id) + if not proj: + raise HTTPException(status_code=404, detail="Project not found") + if proj["status"] != "done": + raise HTTPException(status_code=400, detail=f"렌더링 미완료 (status: {proj['status']})") + meta_path = os.path.join(VIDEO_DATA_DIR, "videos", str(project_id), "metadata.json") + metadata = {} + if os.path.exists(meta_path): + with open(meta_path, encoding="utf-8") as f: + metadata = json.load(f) + thumb_url = proj["output_url"].replace("output.mp4", "thumbnail.jpg") if proj["output_url"] else "" + return { + "project_id": project_id, + "output_url": proj["output_url"], + "thumbnail_url": thumb_url, + "yt_title": proj["yt_title"], + "yt_description": proj["yt_description"], + "yt_tags": proj["yt_tags"], + "metadata": metadata, + } + + +@app.delete("/api/music/video-project/{project_id}") +def delete_project(project_id: int): + if not get_video_project(project_id): + raise HTTPException(status_code=404, detail="Project not found") + out_dir = os.path.join(VIDEO_DATA_DIR, "videos", str(project_id)) + if os.path.isdir(out_dir): + shutil.rmtree(out_dir, ignore_errors=True) + delete_video_project(project_id) + return {"ok": True} + + +# ── 수익화 추적 API ─────────────────────────────────────────────────────────── + +@app.get("/api/music/revenue/dashboard") +def revenue_dashboard(): + return get_revenue_dashboard() + + +@app.get("/api/music/revenue") +def list_revenue(yt_video_id: Optional[str] = None, year_month: Optional[str] = None): + return {"records": get_all_revenue_records(yt_video_id, year_month)} + + +@app.post("/api/music/revenue", status_code=201) +def add_revenue(req: RevenueCreate): + return create_revenue_record(req.model_dump()) + + +@app.put("/api/music/revenue/{record_id}") +def edit_revenue(record_id: int, req: RevenueUpdate): + data = {k: v for k, v in req.model_dump().items() if v is not None} + result = update_revenue_record(record_id, data) + if not result: + raise HTTPException(status_code=404, detail="Record not found") + return result + + +@app.delete("/api/music/revenue/{record_id}") +def remove_revenue(record_id: int): + if not delete_revenue_record(record_id): + raise HTTPException(status_code=404, detail="Record not found") + return {"ok": True}