feat(music-lab): 영상 프로젝트 6개 + 수익화 5개 API 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 11:59:11 +09:00
parent abf475433b
commit 8e7a3806c5

View File

@@ -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}