generated from CubeCraft-Creations/Tracehound
219 lines
6.6 KiB
Go
219 lines
6.6 KiB
Go
// 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)
|
|
}
|