feat(server): FastAPI app with API-key auth and health check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 20:09:50 -07:00
parent 257ed5e0a5
commit 49a45e6270
4 changed files with 74 additions and 0 deletions

37
server/app.py Normal file
View File

@@ -0,0 +1,37 @@
"""MusicFetch REST API. Plain HTTP behind an upstream TLS reverse proxy."""
import os
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.responses import JSONResponse
from . import actions, jobs, mf
API_KEY = os.environ.get("MUSICFETCH_API_KEY", "")
ROOT = os.environ.get("MUSICFETCH_ROOT", "/media/music")
app = FastAPI(title="MusicFetch API")
def require_key(x_api_key: str = Header(default="")):
if not API_KEY or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key.")
@app.exception_handler(HTTPException)
async def _http_exc(_req, exc: HTTPException):
# Always return a Siri-speakable {"message": ...} body.
return JSONResponse(status_code=exc.status_code, content={"message": exc.detail})
@app.get("/health")
def health():
return {"status": "ok"}
# Minimal stub so that auth tests can verify 401 behaviour before the real
# /fetch implementation lands in Task 5. This route MUST carry require_key as
# a dependency so unauthenticated / wrong-key requests are rejected here.
@app.post("/fetch", dependencies=[Depends(require_key)])
def fetch_stub(q: str = ""):
# Task 5 will replace this body with the full search-and-download logic.
raise HTTPException(status_code=501, detail="Not yet implemented.")

5
server/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
requests
ytmusicapi
rich

16
tests/conftest.py Normal file
View File

@@ -0,0 +1,16 @@
import os
import pytest
os.environ.setdefault("MUSICFETCH_API_KEY", "test-key")
@pytest.fixture
def client():
from fastapi.testclient import TestClient
from server.app import app
return TestClient(app)
@pytest.fixture
def auth():
return {"X-API-Key": "test-key"}

16
tests/test_auth.py Normal file
View File

@@ -0,0 +1,16 @@
def test_health_no_auth(client):
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_fetch_requires_key(client):
r = client.post("/fetch", params={"q": "anything"})
assert r.status_code == 401
assert "message" in r.json()
def test_fetch_rejects_wrong_key(client):
r = client.post("/fetch", params={"q": "anything"},
headers={"X-API-Key": "wrong"})
assert r.status_code == 401