feat(music-render): nas_client webhook adapter (SP-5)
NAS DB 직접 접근 불가 → webhook_update_task/webhook_add_track으로 변환. X-Internal-Key 헤더 자동 첨부. 실패 시 raise 안 함 (logger.error). env var는 call time에 읽어 monkeypatch 테스트 호환성 확보. Plan-B-Music Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
services/music-render/nas_client.py
Normal file
80
services/music-render/nas_client.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""NAS webhook 어댑터 — Windows worker가 NAS DB 직접 접근 못하므로 HTTP로 위임.
|
||||
|
||||
기존 NAS suno_provider/local_provider의 `update_task`, `add_track` 호출을
|
||||
이 모듈의 webhook_update_task/webhook_add_track으로 치환.
|
||||
|
||||
webhook 실패는 raise하지 않고 logger.error로 기록 (provider 로직 흐름 유지).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TIMEOUT = 10.0
|
||||
|
||||
|
||||
def _post(payload: Dict[str, Any]) -> None:
|
||||
nas_base_url = os.getenv("NAS_BASE_URL", "http://192.168.45.54:18600")
|
||||
internal_api_key = os.getenv("INTERNAL_API_KEY", "")
|
||||
url = f"{nas_base_url}/api/internal/music/update"
|
||||
try:
|
||||
r = httpx.post(
|
||||
url,
|
||||
headers={"X-Internal-Key": internal_api_key},
|
||||
json=payload,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
logger.error("webhook %s returned %d: %s",
|
||||
payload.get("task_id"), r.status_code, r.text[:200])
|
||||
except Exception:
|
||||
logger.exception("webhook %s 호출 실패", payload.get("task_id"))
|
||||
|
||||
|
||||
def webhook_update_task(
|
||||
task_id: str,
|
||||
status: str,
|
||||
progress: int,
|
||||
message: str = "",
|
||||
audio_url: Optional[str] = None,
|
||||
error: Optional[str] = None,
|
||||
) -> None:
|
||||
"""기존 update_task(task_id, status, progress, message, audio_url=None, error=None) 대체."""
|
||||
payload: Dict[str, Any] = {
|
||||
"task_id": task_id,
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"message": message,
|
||||
}
|
||||
if audio_url is not None:
|
||||
payload["audio_url"] = audio_url
|
||||
if error is not None:
|
||||
payload["error"] = error
|
||||
_post(payload)
|
||||
|
||||
|
||||
def webhook_add_track(
|
||||
task_id: str,
|
||||
status: str,
|
||||
progress: int,
|
||||
message: str = "",
|
||||
audio_url: Optional[str] = None,
|
||||
track: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""update + add_track을 한 webhook 호출로 결합 (NAS internal_router가 둘 다 처리)."""
|
||||
payload: Dict[str, Any] = {
|
||||
"task_id": task_id,
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"message": message,
|
||||
}
|
||||
if audio_url is not None:
|
||||
payload["audio_url"] = audio_url
|
||||
if track is not None:
|
||||
payload["track"] = track
|
||||
_post(payload)
|
||||
79
services/music-render/tests/test_nas_client.py
Normal file
79
services/music-render/tests/test_nas_client.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""nas_client — webhook adapter tests."""
|
||||
import os
|
||||
import pytest
|
||||
import respx
|
||||
import httpx
|
||||
|
||||
from nas_client import webhook_update_task, webhook_add_track
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _env(monkeypatch):
|
||||
monkeypatch.setenv("NAS_BASE_URL", "http://nas-test:18600")
|
||||
monkeypatch.setenv("INTERNAL_API_KEY", "test-key")
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_webhook_update_task_sends_x_internal_key():
|
||||
route = respx.post("http://nas-test:18600/api/internal/music/update").mock(
|
||||
return_value=httpx.Response(200, json={"ok": True})
|
||||
)
|
||||
webhook_update_task("task-1", "processing", 30, message="downloading")
|
||||
assert route.called
|
||||
req = route.calls[0].request
|
||||
assert req.headers["X-Internal-Key"] == "test-key"
|
||||
import json
|
||||
body = json.loads(req.content)
|
||||
assert body["task_id"] == "task-1"
|
||||
assert body["status"] == "processing"
|
||||
assert body["progress"] == 30
|
||||
assert body["message"] == "downloading"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_webhook_update_task_with_audio_url():
|
||||
route = respx.post("http://nas-test:18600/api/internal/music/update").mock(
|
||||
return_value=httpx.Response(200, json={"ok": True})
|
||||
)
|
||||
webhook_update_task("task-2", "succeeded", 100, message="완료",
|
||||
audio_url="/media/music/task-2.mp3")
|
||||
import json
|
||||
payload = json.loads(route.calls[0].request.content)
|
||||
assert payload["audio_url"] == "/media/music/task-2.mp3"
|
||||
assert payload["status"] == "succeeded"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_webhook_update_task_with_error():
|
||||
route = respx.post("http://nas-test:18600/api/internal/music/update").mock(
|
||||
return_value=httpx.Response(200, json={"ok": True})
|
||||
)
|
||||
webhook_update_task("task-3", "failed", 0, error="API rate limit")
|
||||
import json
|
||||
payload = json.loads(route.calls[0].request.content)
|
||||
assert payload["error"] == "API rate limit"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_webhook_add_track_uses_track_field():
|
||||
"""add_track은 update와 동시에 (succeeded 시)."""
|
||||
route = respx.post("http://nas-test:18600/api/internal/music/update").mock(
|
||||
return_value=httpx.Response(200, json={"ok": True})
|
||||
)
|
||||
track = {"title": "x", "audio_url": "/media/music/t.mp3", "provider": "suno"}
|
||||
webhook_add_track("task-4", "succeeded", 100, message="ok",
|
||||
audio_url="/media/music/t.mp3", track=track)
|
||||
import json
|
||||
payload = json.loads(route.calls[0].request.content)
|
||||
assert payload["track"]["title"] == "x"
|
||||
assert payload["status"] == "succeeded"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_webhook_swallows_network_error(caplog):
|
||||
"""webhook 실패해도 raise 안 함 (logger.error)."""
|
||||
respx.post("http://nas-test:18600/api/internal/music/update").mock(
|
||||
side_effect=httpx.ConnectError("no host")
|
||||
)
|
||||
# raise 안 하면 통과
|
||||
webhook_update_task("task-5", "processing", 10)
|
||||
Reference in New Issue
Block a user