This commit is contained in:
2026-06-15 22:46:12 +08:00
commit f6508eccdb
38 changed files with 3133 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
package httpapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"synctv/server/internal/room"
"synctv/server/internal/store"
)
type Rooms interface {
Create(ctx context.Context) (room.Room, error)
Get(ctx context.Context, code string) (room.Room, error)
SetSource(ctx context.Context, code, deviceID string, src room.Source) (room.Room, error)
}
type Server struct {
rooms Rooms
hub http.Handler
proxy http.Handler
}
func New(rooms Rooms, hub http.Handler, proxy http.Handler) *Server {
return &Server{rooms: rooms, hub: hub, proxy: proxy}
}
func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", s.health)
mux.HandleFunc("POST /api/rooms", s.createRoom)
mux.HandleFunc("GET /api/rooms/{code}", s.getRoom)
mux.HandleFunc("POST /api/rooms/{code}/source", s.setSource)
mux.Handle("GET /api/rooms/{code}/stream", s.proxy)
mux.Handle("GET /ws", s.hub)
return withCORS(mux)
}
func (s *Server) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) createRoom(w http.ResponseWriter, r *http.Request) {
rm, err := s.rooms.Create(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{"room": rm})
}
func (s *Server) getRoom(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
deviceID := r.URL.Query().Get("deviceId")
rm, err := s.rooms.Get(r.Context(), code)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, store.ErrNotFound) {
status = http.StatusNotFound
}
writeError(w, status, err.Error())
return
}
writeJSON(w, http.StatusOK, room.Snapshot{
Room: rm.PublicFor(deviceID),
IsOwner: deviceID != "" && rm.OwnerDeviceID == deviceID,
DeviceID: deviceID,
ServerNow: room.NowMS(),
})
}
func (s *Server) setSource(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
deviceID := r.Header.Get("X-Device-Id")
if strings.TrimSpace(deviceID) == "" {
writeError(w, http.StatusBadRequest, "X-Device-Id is required")
return
}
var src room.Source
if err := json.NewDecoder(r.Body).Decode(&src); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
rm, err := s.rooms.SetSource(r.Context(), code, deviceID, src)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, room.Snapshot{
Room: rm.PublicFor(deviceID),
IsOwner: rm.OwnerDeviceID == deviceID,
DeviceID: deviceID,
ServerNow: room.NowMS(),
})
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]any{"error": message})
}
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Device-Id")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}