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