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:
@@ -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 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 [
|
for col, default in [
|
||||||
("provider", "'local'"), ("lyrics", "''"),
|
("provider", "'local'"), ("lyrics", "''"),
|
||||||
@@ -219,3 +231,58 @@ def get_track_file_path(track_id: int) -> Optional[str]:
|
|||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
row = conn.execute("SELECT file_path FROM music_library WHERE id = ?", (track_id,)).fetchone()
|
row = conn.execute("SELECT file_path FROM music_library WHERE id = ?", (track_id,)).fetchone()
|
||||||
return row["file_path"] if row else None
|
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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from .db import (
|
|||||||
create_task, get_task,
|
create_task, get_task,
|
||||||
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_duration,
|
||||||
|
get_all_lyrics, add_lyrics, update_lyrics, delete_lyrics,
|
||||||
)
|
)
|
||||||
from .local_provider import run_local_generation
|
from .local_provider import run_local_generation
|
||||||
from .suno_provider import (
|
from .suno_provider import (
|
||||||
@@ -332,3 +333,47 @@ def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks):
|
|||||||
create_task(task_id, params, provider="suno")
|
create_task(task_id, params, provider="suno")
|
||||||
background_tasks.add_task(run_vocal_removal, task_id, params)
|
background_tasks.add_task(run_vocal_removal, task_id, params)
|
||||||
return {"task_id": task_id, "provider": "suno"}
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user