feat: add FastAPI routes for playlist, shows, admin, and health

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-12 01:40:04 -04:00
parent ab81ad4fd4
commit 2e22a2b3ff
2 changed files with 249 additions and 0 deletions

130
src/ntr_fetcher/api.py Normal file
View File

@@ -0,0 +1,130 @@
import logging
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Depends, Header
from pydantic import BaseModel
from ntr_fetcher.db import Database
from ntr_fetcher.week import get_show_week
logger = logging.getLogger(__name__)
class RefreshRequest(BaseModel):
full: bool = False
class AddTrackRequest(BaseModel):
soundcloud_url: str | None = None
track_id: int | None = None
position: int | None = None
class MoveTrackRequest(BaseModel):
position: int
def create_app(db: Database, poller, admin_token: str) -> FastAPI:
app = FastAPI(title="NtR SoundCloud Fetcher")
def _require_admin(authorization: str | None = Header(None)):
if authorization is None or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid token")
token = authorization.removeprefix("Bearer ")
if token != admin_token:
raise HTTPException(status_code=401, detail="Invalid token")
def _current_show():
now = datetime.now(timezone.utc)
week_start, week_end = get_show_week(now, show_day=2, show_hour=22)
return db.get_or_create_show(week_start, week_end)
@app.get("/health")
def health():
show = _current_show()
tracks = db.get_show_tracks(show.id)
return {
"status": "ok",
"poller_alive": poller.alive,
"last_fetch": poller.last_fetch.isoformat() if poller.last_fetch else None,
"current_week_track_count": len(tracks),
}
@app.get("/playlist")
def playlist():
show = _current_show()
tracks = db.get_show_tracks(show.id)
return {
"show_id": show.id,
"week_start": show.week_start.isoformat(),
"week_end": show.week_end.isoformat(),
"tracks": tracks,
}
@app.get("/playlist/{position}")
def playlist_track(position: int):
show = _current_show()
track = db.get_show_track_by_position(show.id, position)
if track is None:
raise HTTPException(status_code=404, detail=f"No track at position {position}")
return track
@app.get("/shows")
def list_shows(limit: int = 20, offset: int = 0):
shows = db.list_shows(limit=limit, offset=offset)
return [
{
"id": s.id,
"week_start": s.week_start.isoformat(),
"week_end": s.week_end.isoformat(),
"created_at": s.created_at.isoformat(),
}
for s in shows
]
@app.get("/shows/{show_id}")
def show_detail(show_id: int):
shows = db.list_shows(limit=1000, offset=0)
show = next((s for s in shows if s.id == show_id), None)
if show is None:
raise HTTPException(status_code=404, detail="Show not found")
tracks = db.get_show_tracks(show.id)
return {
"show_id": show.id,
"week_start": show.week_start.isoformat(),
"week_end": show.week_end.isoformat(),
"tracks": tracks,
}
@app.post("/admin/refresh")
async def admin_refresh(body: RefreshRequest = RefreshRequest(), _=Depends(_require_admin)):
await poller.poll_once(full=body.full)
show = _current_show()
tracks = db.get_show_tracks(show.id)
return {"status": "refreshed", "track_count": len(tracks)}
@app.post("/admin/tracks")
def admin_add_track(body: AddTrackRequest, _=Depends(_require_admin)):
show = _current_show()
if body.track_id is not None:
db.add_track_to_show(show.id, body.track_id, body.position)
return {"status": "added"}
raise HTTPException(status_code=400, detail="Provide track_id")
@app.delete("/admin/tracks/{track_id}")
def admin_remove_track(track_id: int, _=Depends(_require_admin)):
show = _current_show()
if not db.has_track_in_show(show.id, track_id):
raise HTTPException(status_code=404, detail="Track not in current show")
db.remove_show_track(show.id, track_id)
return {"status": "removed"}
@app.put("/admin/tracks/{track_id}/position")
def admin_move_track(track_id: int, body: MoveTrackRequest, _=Depends(_require_admin)):
show = _current_show()
if not db.has_track_in_show(show.id, track_id):
raise HTTPException(status_code=404, detail="Track not in current show")
db.move_show_track(show.id, track_id, body.position)
return {"status": "moved"}
return app