2026-03-12 01:40:04 -04:00
|
|
|
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):
|
2026-03-12 01:46:23 -04:00
|
|
|
track_id: int
|
2026-03-12 01:40:04 -04:00
|
|
|
position: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MoveTrackRequest(BaseModel):
|
|
|
|
|
position: int
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 01:46:23 -04:00
|
|
|
def create_app(
|
|
|
|
|
db: Database,
|
|
|
|
|
poller,
|
|
|
|
|
admin_token: str,
|
|
|
|
|
show_day: int = 2,
|
|
|
|
|
show_hour: int = 22,
|
|
|
|
|
) -> FastAPI:
|
2026-03-12 01:40:04 -04:00
|
|
|
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)
|
2026-03-12 01:46:23 -04:00
|
|
|
week_start, week_end = get_show_week(now, show_day=show_day, show_hour=show_hour)
|
2026-03-12 01:40:04 -04:00
|
|
|
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,
|
2026-03-12 02:09:15 -04:00
|
|
|
"episode_number": show.episode_number,
|
2026-03-12 01:40:04 -04:00
|
|
|
"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,
|
2026-03-12 02:09:15 -04:00
|
|
|
"episode_number": s.episode_number,
|
2026-03-12 01:40:04 -04:00
|
|
|
"week_start": s.week_start.isoformat(),
|
|
|
|
|
"week_end": s.week_end.isoformat(),
|
|
|
|
|
"created_at": s.created_at.isoformat(),
|
|
|
|
|
}
|
|
|
|
|
for s in shows
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-12 02:33:42 -04:00
|
|
|
@app.get("/shows/by-episode/{episode_number}")
|
|
|
|
|
def show_by_episode(episode_number: int):
|
|
|
|
|
show = db.get_show_by_episode_number(episode_number)
|
|
|
|
|
if show is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"No show with episode number {episode_number}")
|
|
|
|
|
tracks = db.get_show_tracks(show.id)
|
|
|
|
|
return {
|
|
|
|
|
"show_id": show.id,
|
|
|
|
|
"episode_number": show.episode_number,
|
|
|
|
|
"week_start": show.week_start.isoformat(),
|
|
|
|
|
"week_end": show.week_end.isoformat(),
|
|
|
|
|
"tracks": tracks,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 01:40:04 -04:00
|
|
|
@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,
|
2026-03-12 02:09:15 -04:00
|
|
|
"episode_number": show.episode_number,
|
2026-03-12 01:40:04 -04:00
|
|
|
"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()
|
2026-03-12 01:46:23 -04:00
|
|
|
db.add_track_to_show(show.id, body.track_id, body.position)
|
|
|
|
|
return {"status": "added"}
|
2026-03-12 01:40:04 -04:00
|
|
|
|
|
|
|
|
@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
|