feat(music-lab): 영상 프로젝트 6개 + 수익화 5개 API 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
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,
|
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,
|
update_track_duration, update_track_file_info, update_track_hash,
|
||||||
get_all_lyrics, add_lyrics, update_lyrics, delete_lyrics,
|
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 .local_provider import run_local_generation
|
||||||
from .suno_provider import (
|
from .suno_provider import (
|
||||||
@@ -33,6 +39,7 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
MUSIC_DATA_DIR = "/app/data"
|
MUSIC_DATA_DIR = "/app/data"
|
||||||
|
VIDEO_DATA_DIR = os.getenv("VIDEO_DATA_DIR", "/app/data")
|
||||||
|
|
||||||
|
|
||||||
def _get_mp3_duration(file_path: str) -> Optional[int]:
|
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):
|
if not delete_lyrics(lyrics_id):
|
||||||
raise HTTPException(status_code=404, detail="Lyrics not found")
|
raise HTTPException(status_code=404, detail="Lyrics not found")
|
||||||
return {"ok": True}
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user