music-lab: 가사 저장/수정/삭제 CRUD API 추가

- saved_lyrics 테이블 (id, title, text, prompt, created_at, updated_at)
- GET /api/music/lyrics/library — 저장된 가사 목록 조회
- POST /api/music/lyrics/library — 가사 저장
- PUT /api/music/lyrics/library/:id — 가사 수정
- DELETE /api/music/lyrics/library/:id — 가사 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 19:11:39 +09:00
parent 649b99d143
commit bb76e62774
2 changed files with 112 additions and 0 deletions

View File

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

View File

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