diff --git a/music-lab/app/db.py b/music-lab/app/db.py index 8e6dc4f..f0d1d1c 100644 --- a/music-lab/app/db.py +++ b/music-lab/app/db.py @@ -56,6 +56,18 @@ def init_db() -> None: """) conn.execute("CREATE INDEX IF NOT EXISTS idx_library_created ON music_library(created_at DESC)") + conn.execute(""" + CREATE TABLE IF NOT EXISTS saved_lyrics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL DEFAULT '', + text TEXT NOT NULL DEFAULT '', + prompt TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_lyrics_created ON saved_lyrics(created_at DESC)") + # 기존 테이블 마이그레이션 (컬럼 없으면 추가) for col, default in [ ("provider", "'local'"), ("lyrics", "''"), @@ -219,3 +231,58 @@ def get_track_file_path(track_id: int) -> Optional[str]: with _conn() as conn: row = conn.execute("SELECT file_path FROM music_library WHERE id = ?", (track_id,)).fetchone() return row["file_path"] if row else None + + +# ── saved_lyrics CRUD ──────────────────────────────────────────────────────── + +def _lyrics_row_to_dict(r) -> Dict[str, Any]: + return { + "id": r["id"], + "title": r["title"], + "text": r["text"], + "prompt": r["prompt"], + "created_at": r["created_at"], + "updated_at": r["updated_at"], + } + + +def get_all_lyrics() -> List[Dict[str, Any]]: + with _conn() as conn: + rows = conn.execute("SELECT * FROM saved_lyrics ORDER BY created_at DESC").fetchall() + return [_lyrics_row_to_dict(r) for r in rows] + + +def add_lyrics(data: Dict[str, Any]) -> Dict[str, Any]: + with _conn() as conn: + conn.execute( + "INSERT INTO saved_lyrics (title, text, prompt) VALUES (?, ?, ?)", + (data.get("title", ""), data.get("text", ""), data.get("prompt", "")), + ) + row = conn.execute("SELECT * FROM saved_lyrics WHERE rowid = last_insert_rowid()").fetchone() + return _lyrics_row_to_dict(row) + + +def update_lyrics(lyrics_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + with _conn() as conn: + fields = [] + values = [] + for k in ("title", "text", "prompt"): + if k in data: + fields.append(f"{k} = ?") + values.append(data[k]) + if not fields: + return None + fields.append("updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')") + values.append(lyrics_id) + conn.execute(f"UPDATE saved_lyrics SET {', '.join(fields)} WHERE id = ?", values) + row = conn.execute("SELECT * FROM saved_lyrics WHERE id = ?", (lyrics_id,)).fetchone() + return _lyrics_row_to_dict(row) if row else None + + +def delete_lyrics(lyrics_id: int) -> bool: + with _conn() as conn: + row = conn.execute("SELECT id FROM saved_lyrics WHERE id = ?", (lyrics_id,)).fetchone() + if not row: + return False + conn.execute("DELETE FROM saved_lyrics WHERE id = ?", (lyrics_id,)) + return True diff --git a/music-lab/app/main.py b/music-lab/app/main.py index 5c0c4ef..47eac85 100644 --- a/music-lab/app/main.py +++ b/music-lab/app/main.py @@ -10,6 +10,7 @@ from .db import ( create_task, get_task, get_all_tracks, add_track, delete_track, get_track_file_path, get_track_by_task_id, update_track_duration, + get_all_lyrics, add_lyrics, update_lyrics, delete_lyrics, ) from .local_provider import run_local_generation from .suno_provider import ( @@ -332,3 +333,47 @@ def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks): create_task(task_id, params, provider="suno") background_tasks.add_task(run_vocal_removal, task_id, params) return {"task_id": task_id, "provider": "suno"} + + +# ── 저장된 가사 CRUD API ──────────────────────────────────────────────────── + +class LyricsSave(BaseModel): + title: str = "" + text: str = "" + prompt: str = "" + + +class LyricsUpdate(BaseModel): + title: Optional[str] = None + text: Optional[str] = None + prompt: Optional[str] = None + + +@app.get("/api/music/lyrics/library") +def list_saved_lyrics(): + """저장된 가사 목록 전체 조회 (생성일 내림차순).""" + return {"lyrics": get_all_lyrics()} + + +@app.post("/api/music/lyrics/library", status_code=201) +def save_lyrics(req: LyricsSave): + """가사 저장.""" + return add_lyrics(req.model_dump()) + + +@app.put("/api/music/lyrics/library/{lyrics_id}") +def edit_lyrics(lyrics_id: int, req: LyricsUpdate): + """가사 수정.""" + data = {k: v for k, v in req.model_dump().items() if v is not None} + result = update_lyrics(lyrics_id, data) + if not result: + raise HTTPException(status_code=404, detail="Lyrics not found") + return result + + +@app.delete("/api/music/lyrics/library/{lyrics_id}") +def remove_lyrics(lyrics_id: int): + """가사 삭제.""" + if not delete_lyrics(lyrics_id): + raise HTTPException(status_code=404, detail="Lyrics not found") + return {"ok": True}