Added archiving endpoint
This commit is contained in:
parent
57d7faa6ca
commit
df9c38de7a
@ -19,6 +19,7 @@ func NewRouter(svc service.EpisodeService) *gin.Engine {
|
|||||||
|
|
||||||
v1 := r.Group("/v1")
|
v1 := r.Group("/v1")
|
||||||
|
|
||||||
|
// GET /v1/current
|
||||||
v1.GET("/current", func(c *gin.Context) {
|
v1.GET("/current", func(c *gin.Context) {
|
||||||
cur, err := svc.GetCurrent(c.Request.Context())
|
cur, err := svc.GetCurrent(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -32,6 +33,7 @@ func NewRouter(svc service.EpisodeService) *gin.Engine {
|
|||||||
c.JSON(http.StatusOK, cur)
|
c.JSON(http.StatusOK, cur)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// POST /v1/current (strict HH:MM:SS already enforced at service layer)
|
||||||
type setCurrentReq struct {
|
type setCurrentReq struct {
|
||||||
ID int64 `json:"id" binding:"required"`
|
ID int64 `json:"id" binding:"required"`
|
||||||
StartTime string `json:"start_time" binding:"required"`
|
StartTime string `json:"start_time" binding:"required"`
|
||||||
@ -60,5 +62,41 @@ func NewRouter(svc service.EpisodeService) *gin.Engine {
|
|||||||
c.JSON(http.StatusOK, cur)
|
c.JSON(http.StatusOK, cur)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// POST /v1/archive (move one or many IDs from current → current_archive)
|
||||||
|
type moveReq struct {
|
||||||
|
ID *int64 `json:"id,omitempty"`
|
||||||
|
IDs []int64 `json:"ids,omitempty"`
|
||||||
|
}
|
||||||
|
v1.POST("/archive", func(c *gin.Context) {
|
||||||
|
var req moveReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids := make([]int64, 0, len(req.IDs)+1)
|
||||||
|
if req.ID != nil {
|
||||||
|
ids = append(ids, *req.ID)
|
||||||
|
}
|
||||||
|
ids = append(ids, req.IDs...)
|
||||||
|
|
||||||
|
res, err := svc.MoveToArchive(c.Request.Context(), ids)
|
||||||
|
if err != nil {
|
||||||
|
if err == service.ErrEmptyIDs {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "move failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"moved_ids": res.MovedIDs,
|
||||||
|
"deleted_ids": res.DeletedIDs,
|
||||||
|
"skipped_ids": res.SkippedIDs,
|
||||||
|
"inserted": len(res.MovedIDs),
|
||||||
|
"deleted": len(res.DeletedIDs),
|
||||||
|
"skipped": len(res.SkippedIDs),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,16 @@ type Episode struct {
|
|||||||
DateCreated time.Time `json:"date_created"`
|
DateCreated time.Time `json:"date_created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoveResult struct {
|
||||||
|
MovedIDs []int64 `json:"moved_ids"`
|
||||||
|
DeletedIDs []int64 `json:"deleted_ids"`
|
||||||
|
SkippedIDs []int64 `json:"skipped_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
type EpisodeRepository interface {
|
type EpisodeRepository interface {
|
||||||
GetCurrent(ctx context.Context) (Episode, error)
|
GetCurrent(ctx context.Context) (Episode, error)
|
||||||
SetCurrent(ctx context.Context, id int64, startHHMMSS string) error
|
SetCurrent(ctx context.Context, id int64, startHHMMSS string) error
|
||||||
|
MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pgxEpisodeRepo struct {
|
type pgxEpisodeRepo struct {
|
||||||
@ -83,3 +90,79 @@ func (r *pgxEpisodeRepo) SetCurrent(ctx context.Context, id int64, startHHMMSS s
|
|||||||
|
|
||||||
return tx.Commit(ctx)
|
return tx.Commit(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *pgxEpisodeRepo) MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error) {
|
||||||
|
res := MoveResult{}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := r.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
// 1) Insert into archive from current (skip duplicates by id)
|
||||||
|
insRows, err := tx.Query(ctx, `
|
||||||
|
INSERT INTO current_archive (
|
||||||
|
id, ep_num, ep_title, season_name, start_time, playback_length, current_ep, date_created
|
||||||
|
)
|
||||||
|
SELECT id, ep_num, ep_title, season_name, start_time, playback_length, current_ep, date_created
|
||||||
|
FROM current
|
||||||
|
WHERE id = ANY($1::bigint[])
|
||||||
|
ON CONFLICT (id) DO NOTHING
|
||||||
|
RETURNING id
|
||||||
|
`, ids)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
for insRows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := insRows.Scan(&id); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
res.MovedIDs = append(res.MovedIDs, id)
|
||||||
|
}
|
||||||
|
if err := insRows.Err(); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Delete from current only those actually inserted to archive
|
||||||
|
if len(res.MovedIDs) > 0 {
|
||||||
|
delRows, err := tx.Query(ctx, `
|
||||||
|
DELETE FROM current
|
||||||
|
WHERE id = ANY($1::bigint[])
|
||||||
|
RETURNING id
|
||||||
|
`, res.MovedIDs)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
for delRows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := delRows.Scan(&id); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
res.DeletedIDs = append(res.DeletedIDs, id)
|
||||||
|
}
|
||||||
|
if err := delRows.Err(); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Compute skipped = requested - moved
|
||||||
|
sel := make(map[int64]struct{}, len(res.MovedIDs))
|
||||||
|
for _, id := range res.MovedIDs {
|
||||||
|
sel[id] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
if _, ok := sel[id]; !ok {
|
||||||
|
res.SkippedIDs = append(res.SkippedIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -9,11 +9,15 @@ import (
|
|||||||
"watch-party-backend/internal/repo"
|
"watch-party-backend/internal/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidTime = errors.New("invalid start_time (expected HH:MM:SS)")
|
var (
|
||||||
|
ErrInvalidTime = errors.New("invalid start_time (expected HH:MM:SS)")
|
||||||
|
ErrEmptyIDs = errors.New("ids must not be empty")
|
||||||
|
)
|
||||||
|
|
||||||
type EpisodeService interface {
|
type EpisodeService interface {
|
||||||
GetCurrent(ctx context.Context) (repo.Episode, error)
|
GetCurrent(ctx context.Context) (repo.Episode, error)
|
||||||
SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error)
|
SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error)
|
||||||
|
MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type episodeService struct {
|
type episodeService struct {
|
||||||
@ -30,28 +34,34 @@ func (s *episodeService) GetCurrent(ctx context.Context) (repo.Episode, error) {
|
|||||||
return s.repo.GetCurrent(c)
|
return s.repo.GetCurrent(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hhmmss = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$`)
|
||||||
|
|
||||||
func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) {
|
func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) {
|
||||||
// Strict HH:MM:SS (24h, zero-padded) — e.g., 20:07:05
|
if !hhmmss.MatchString(start) {
|
||||||
if !isHHMMSS(start) {
|
|
||||||
return repo.Episode{}, ErrInvalidTime
|
return repo.Episode{}, ErrInvalidTime
|
||||||
}
|
}
|
||||||
|
|
||||||
c, cancel := context.WithTimeout(ctx, 5*time.Second)
|
c, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := s.repo.SetCurrent(c, id, start); err != nil {
|
if err := s.repo.SetCurrent(c, id, start); err != nil {
|
||||||
return repo.Episode{}, err
|
return repo.Episode{}, err
|
||||||
}
|
}
|
||||||
// return the new current row
|
|
||||||
return s.repo.GetCurrent(c)
|
return s.repo.GetCurrent(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hhmmss = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$`)
|
func (s *episodeService) MoveToArchive(ctx context.Context, ids []int64) (repo.MoveResult, error) {
|
||||||
|
uniq := make([]int64, 0, len(ids))
|
||||||
func isHHMMSS(s string) bool {
|
seen := make(map[int64]struct{}, len(ids))
|
||||||
if !hhmmss.MatchString(s) {
|
for _, id := range ids {
|
||||||
return false
|
if _, ok := seen[id]; !ok {
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
uniq = append(uniq, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err := time.Parse("15:04:05", s)
|
if len(uniq) == 0 {
|
||||||
return err == nil
|
return repo.MoveResult{}, ErrEmptyIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return s.repo.MoveToArchive(c, uniq)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user