Files

219 lines
6.6 KiB
Go
Raw Permalink Normal View History

// Package api provides HTTP handlers for camera operations.
package api
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"github.com/cubecraft/remoterig/internal/db"
"github.com/cubecraft/remoterig/pkg/models"
"github.com/go-chi/chi/v5"
)
// ListCameras returns a handler for GET /cameras.
// Returns all cameras with their latest status from status_logs.
func ListCameras(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Query: join cameras with their most recent status_logs row
rows, err := database.QueryContext(ctx, `
SELECT
c.camera_id,
c.friendly_name,
s.battery_pct,
s.video_remaining_sec,
s.recording_state,
s.mode,
s.resolution,
s.fps,
s.online,
s.recorded_at
FROM cameras c
LEFT JOIN (
SELECT camera_id, battery_pct, video_remaining_sec, recording_state,
mode, resolution, fps, online, recorded_at,
ROW_NUMBER() OVER (PARTITION BY camera_id ORDER BY recorded_at DESC) as rn
FROM status_logs
) s ON c.camera_id = s.camera_id AND s.rn = 1
ORDER BY c.camera_id
`)
if err != nil {
log.Printf("Error querying cameras: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
defer rows.Close()
var statuses []models.CameraStatus
for rows.Next() {
var sl models.StatusLog
var c models.Camera
if err := rows.Scan(
&c.CameraID, &c.FriendlyName,
&sl.BatteryPct, &sl.VideoRemainingSec,
&sl.RecordingState, &sl.Mode, &sl.Resolution, &sl.FPS,
&sl.Online, &sl.RecordedAt,
); err != nil {
log.Printf("Error scanning camera row: %v", err)
continue
}
statuses = append(statuses, models.NewCameraStatus(c, sl))
}
if err := rows.Err(); err != nil {
log.Printf("Error iterating camera rows: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
if statuses == nil {
statuses = []models.CameraStatus{}
}
respondJSON(w, http.StatusOK, statuses)
}
}
// RegisterCamera returns a handler for POST /cameras.
func RegisterCamera(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
CameraID string `json:"camera_id"`
FriendlyName string `json:"friendly_name"`
MacAddress *string `json:"mac_address,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if req.CameraID == "" || req.FriendlyName == "" {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "camera_id and friendly_name are required"})
return
}
_, err := database.ExecContext(r.Context(), `
INSERT INTO cameras (camera_id, friendly_name, mac_address)
VALUES (?, ?, ?)
`, req.CameraID, req.FriendlyName, req.MacAddress)
if err != nil {
if err.Error() == "UNIQUE constraint failed: cameras.mac_address" {
respondJSON(w, http.StatusConflict, map[string]string{"error": "camera with this mac_address already registered"})
return
}
log.Printf("Error registering camera: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
log.Printf("Registered camera %s (%s)", req.CameraID, req.FriendlyName)
resp := map[string]interface{}{
"camera_id": req.CameraID,
"friendly_name": req.FriendlyName,
}
if req.MacAddress != nil {
resp["mac_address"] = *req.MacAddress
}
respondJSON(w, http.StatusCreated, resp)
}
}
// GetCameraDetail returns a handler for GET /cameras/{id}.
func GetCameraDetail(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cameraID := chi.URLParam(r, "id")
if cameraID == "" {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "camera_id required"})
return
}
// Get camera info
var c models.Camera
err := database.QueryRowContext(r.Context(), `
SELECT camera_id, friendly_name, mac_address, created_at, updated_at
FROM cameras WHERE camera_id = ?
`, cameraID).Scan(
&c.CameraID, &c.FriendlyName, &c.MacAddress,
&c.CreatedAt, &c.UpdatedAt,
)
if err == sql.ErrNoRows {
respondJSON(w, http.StatusNotFound, map[string]string{"error": "camera not found"})
return
}
if err != nil {
log.Printf("Error querying camera: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
// Get latest status
var sl models.StatusLog
err = database.QueryRowContext(r.Context(), `
SELECT id, camera_id, recorded_at, battery_pct, video_remaining_sec,
recording_state, mode, resolution, fps, online, raw_battery_pct
FROM status_logs
WHERE camera_id = ?
ORDER BY recorded_at DESC
LIMIT 1
`, cameraID).Scan(
&sl.ID, &sl.CameraID, &sl.RecordedAt,
&sl.BatteryPct, &sl.VideoRemainingSec,
&sl.RecordingState, &sl.Mode, &sl.Resolution, &sl.FPS,
&sl.Online, &sl.RawBatteryPct,
)
if err != nil && err != sql.ErrNoRows {
log.Printf("Error querying latest status: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
// Get 24h history
historyRows, err := database.QueryContext(r.Context(), `
SELECT id, camera_id, recorded_at, battery_pct, video_remaining_sec,
recording_state, mode, resolution, fps, online, raw_battery_pct
FROM status_logs
WHERE camera_id = ? AND recorded_at >= datetime('now', '-24 hours')
ORDER BY recorded_at DESC
LIMIT 100
`, cameraID)
if err != nil {
log.Printf("Error querying history: %v", err)
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
return
}
defer historyRows.Close()
var history []models.StatusLog
for historyRows.Next() {
var h models.StatusLog
if err := historyRows.Scan(
&h.ID, &h.CameraID, &h.RecordedAt,
&h.BatteryPct, &h.VideoRemainingSec,
&h.RecordingState, &h.Mode, &h.Resolution, &h.FPS,
&h.Online, &h.RawBatteryPct,
); err != nil {
continue
}
history = append(history, h)
}
if history == nil {
history = []models.StatusLog{}
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"camera": c,
"last_status": sl,
"history": history,
})
}
}
// respondJSON writes a JSON response with the given status code.
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}