diff --git a/backend/internal/http/handlers.go b/backend/internal/http/handlers.go index 9236095..54b1079 100644 --- a/backend/internal/http/handlers.go +++ b/backend/internal/http/handlers.go @@ -30,5 +30,33 @@ func NewRouter(svc service.EpisodeService) *gin.Engine { c.JSON(http.StatusOK, cur) }) + type setCurrentReq struct { + ID int64 `json:"id" binding:"required"` + StartTime string `json:"start_time" binding:"required"` // HH:MM or HH:MM:SS + } + + r.POST("/current", func(c *gin.Context) { + var req setCurrentReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + cur, err := svc.SetCurrent(c.Request.Context(), req.ID, req.StartTime) + if err != nil { + switch err { + case service.ErrInvalidTime: + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + case repo.ErrNotFound: + c.JSON(http.StatusNotFound, gin.H{"error": "id not found"}) + return + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"}) + return + } + } + c.JSON(http.StatusOK, cur) + }) + return r } diff --git a/backend/internal/repo/episode_repo.go b/backend/internal/repo/episode_repo.go index ffd6813..5973b1f 100644 --- a/backend/internal/repo/episode_repo.go +++ b/backend/internal/repo/episode_repo.go @@ -22,6 +22,7 @@ type Episode struct { type EpisodeRepository interface { GetCurrent(ctx context.Context) (Episode, error) + SetCurrent(ctx context.Context, id int64, startHHMMSS string) error } type pgxEpisodeRepo struct { @@ -58,3 +59,27 @@ LIMIT 1; } return e, nil } + +func (r *pgxEpisodeRepo) SetCurrent(ctx context.Context, id int64, startHHMMSS string) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer func() { _ = tx.Rollback(ctx) }() + + // 1) clear any current episode flag + if _, err := tx.Exec(ctx, `UPDATE current SET current_ep = false WHERE current_ep = true`); err != nil { + return err + } + + // 2) set start_time and current_ep for the id + ct, err := tx.Exec(ctx, `UPDATE current SET start_time = $1::time, current_ep = true WHERE id = $2`, startHHMMSS, id) + if err != nil { + return err + } + if ct.RowsAffected() == 0 { + return ErrNotFound + } + + return tx.Commit(ctx) +} diff --git a/backend/internal/service/episode_service.go b/backend/internal/service/episode_service.go index 72c0c96..5758cae 100644 --- a/backend/internal/service/episode_service.go +++ b/backend/internal/service/episode_service.go @@ -2,13 +2,18 @@ package service import ( "context" + "errors" + "strings" "time" "watch-party-backend/internal/repo" ) +var ErrInvalidTime = errors.New("invalid start_time (expected HH:MM or HH:MM:SS)") + type EpisodeService interface { GetCurrent(ctx context.Context) (repo.Episode, error) + SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) } type episodeService struct { @@ -20,8 +25,38 @@ func NewEpisodeService(r repo.EpisodeRepository) EpisodeService { } func (s *episodeService) GetCurrent(ctx context.Context) (repo.Episode, error) { - // Add cross-cutting concerns here (timeouts, metrics, caching, etc.) c, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() return s.repo.GetCurrent(c) } + +func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) { + // normalize and validate time as HH:MM:SS + normalized, err := normalizeHHMMSS(start) + if err != nil { + return repo.Episode{}, ErrInvalidTime + } + + c, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // do the transaction + if err := s.repo.SetCurrent(c, id, normalized); err != nil { + return repo.Episode{}, err + } + // return the new current row + return s.repo.GetCurrent(c) +} + +func normalizeHHMMSS(s string) (string, error) { + s = strings.TrimSpace(s) + if len(s) == 5 { // HH:MM → append seconds + s += ":00" + } + // Strict parse/format to 24h + t, err := time.Parse("15:04:05", s) + if err != nil { + return "", err + } + return t.Format("15:04:05"), nil +}