package handlers import ( "net/http" "os" "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // LiveStreamHandler handles live stream HTTP requests type LiveStreamHandler struct { service *services.LiveStreamService logger *zap.Logger } // NewLiveStreamHandler creates a new LiveStreamHandler func NewLiveStreamHandler(service *services.LiveStreamService, logger *zap.Logger) *LiveStreamHandler { return &LiveStreamHandler{service: service, logger: logger} } // CreateLiveStreamRequest represents the request body for creating a live stream type CreateLiveStreamRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description"` Category string `json:"category"` ThumbnailURL string `json:"thumbnailUrl"` StreamerName string `json:"streamer"` Tags []string `json:"tags"` } // ListLiveStreams returns all live streams (public - optionally filter by is_live) func (h *LiveStreamHandler) ListLiveStreams(c *gin.Context) { var isLive *bool if q := c.Query("is_live"); q != "" { b, err := strconv.ParseBool(q) if err == nil { isLive = &b } } streams, err := h.service.List(c.Request.Context(), isLive) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list streams", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"streams": streams}) } // GetLiveStream returns a single live stream by ID func (h *LiveStreamHandler) GetLiveStream(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid stream ID")) return } stream, err := h.service.Get(c.Request.Context(), id) if err != nil { RespondWithAppError(c, apperrors.NewNotFoundError("stream not found")) return } RespondSuccess(c, http.StatusOK, gin.H{"stream": stream}) } // CreateLiveStream creates a new live stream (requires auth) func (h *LiveStreamHandler) CreateLiveStream(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req CreateLiveStreamRequest if appErr := NewCommonHandler(h.logger).BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } stream := &models.LiveStream{ Title: req.Title, Description: req.Description, Category: req.Category, ThumbnailURL: req.ThumbnailURL, StreamerName: req.StreamerName, Tags: req.Tags, } if stream.StreamerName == "" { stream.StreamerName = "Streamer" } created, err := h.service.Create(c.Request.Context(), userID, stream) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create stream", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{"stream": created}) } // GetMyStreams returns the authenticated user's streams (including stream_key) func (h *LiveStreamHandler) GetMyStreams(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } streams, err := h.service.ListByUser(c.Request.Context(), userID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list streams", err)) return } type streamWithKey struct { *models.LiveStream StreamKey string `json:"stream_key"` } result := make([]streamWithKey, len(streams)) for i, s := range streams { result[i] = streamWithKey{LiveStream: s, StreamKey: s.StreamKey} } RespondSuccess(c, http.StatusOK, gin.H{"streams": result}) } // GetMyStreamKey returns the user's stream key (creates draft if none exist) func (h *LiveStreamHandler) GetMyStreamKey(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } streams, err := h.service.ListByUser(c.Request.Context(), userID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get streams", err)) return } var streamKey string if len(streams) > 0 { streamKey = streams[0].StreamKey } else { draft := &models.LiveStream{Title: "My Stream"} created, createErr := h.service.Create(c.Request.Context(), userID, draft) if createErr != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create stream", createErr)) return } streamKey = created.StreamKey } rtmpHost := os.Getenv("NGINX_RTMP_HOST") if rtmpHost == "" { rtmpHost = "stream.veza.app" } RespondSuccess(c, http.StatusOK, gin.H{ "stream_key": streamKey, "rtmp_url": "rtmp://" + rtmpHost + "/live", }) } // RegenerateStreamKey generates a new stream key for the user's first stream func (h *LiveStreamHandler) RegenerateStreamKey(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } streams, err := h.service.ListByUser(c.Request.Context(), userID) if err != nil || len(streams) == 0 { RespondWithAppError(c, apperrors.NewNotFoundError("no stream found")) return } newKey, err := h.service.RegenerateStreamKey(c.Request.Context(), streams[0].ID, userID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to regenerate key", err)) return } rtmpHost := os.Getenv("NGINX_RTMP_HOST") if rtmpHost == "" { rtmpHost = "stream.veza.app" } RespondSuccess(c, http.StatusOK, gin.H{ "stream_key": newKey, "rtmp_url": "rtmp://" + rtmpHost + "/live", }) } // UpdateLiveStream updates a live stream's metadata (ownership check) func (h *LiveStreamHandler) UpdateLiveStream(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } id, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid stream ID")) return } var req CreateLiveStreamRequest if appErr := NewCommonHandler(h.logger).BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } stream := &models.LiveStream{ Title: req.Title, Description: req.Description, Category: req.Category, ThumbnailURL: req.ThumbnailURL, StreamerName: req.StreamerName, Tags: req.Tags, } updated, updateErr := h.service.Update(c.Request.Context(), id, userID, stream) if updateErr != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update stream", updateErr)) return } RespondSuccess(c, http.StatusOK, gin.H{"stream": updated}) }