feat(queue): add backend queue API with CRUD operations
This commit is contained in:
parent
dc81f89338
commit
6a53daab59
5 changed files with 347 additions and 0 deletions
|
|
@ -293,6 +293,9 @@ func (r *APIRouter) Setup(router *gin.Engine) error {
|
|||
// Inventory / Gear Routes
|
||||
r.setupGearRoutes(v1)
|
||||
|
||||
// Queue Routes
|
||||
r.setupQueueRoutes(v1)
|
||||
|
||||
// Live Streams Routes
|
||||
r.setupLiveRoutes(v1)
|
||||
|
||||
|
|
|
|||
28
veza-backend-api/internal/api/routes_queue.go
Normal file
28
veza-backend-api/internal/api/routes_queue.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"veza-backend-api/internal/handlers"
|
||||
"veza-backend-api/internal/services"
|
||||
)
|
||||
|
||||
// setupQueueRoutes configures the user playback queue routes
|
||||
func (r *APIRouter) setupQueueRoutes(router *gin.RouterGroup) {
|
||||
if r.config == nil || r.config.AuthMiddleware == nil {
|
||||
return
|
||||
}
|
||||
queueService := services.NewQueueService(r.db.GormDB, r.logger)
|
||||
queueHandler := handlers.NewQueueHandler(queueService, r.logger)
|
||||
|
||||
queue := router.Group("/queue")
|
||||
queue.Use(r.config.AuthMiddleware.RequireAuth())
|
||||
r.applyCSRFProtection(queue)
|
||||
{
|
||||
queue.GET("", queueHandler.GetQueue)
|
||||
queue.PUT("", queueHandler.UpdateQueue)
|
||||
queue.POST("/items", queueHandler.AddQueueItem)
|
||||
queue.DELETE("/items/:id", queueHandler.RemoveQueueItem)
|
||||
queue.DELETE("", queueHandler.ClearQueue)
|
||||
}
|
||||
}
|
||||
117
veza-backend-api/internal/handlers/queue_handler.go
Normal file
117
veza-backend-api/internal/handlers/queue_handler.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
apperrors "veza-backend-api/internal/errors"
|
||||
"veza-backend-api/internal/services"
|
||||
)
|
||||
|
||||
// QueueHandler handles queue HTTP requests
|
||||
type QueueHandler struct {
|
||||
queueService *services.QueueService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewQueueHandler creates a new QueueHandler
|
||||
func NewQueueHandler(queueService *services.QueueService, logger *zap.Logger) *QueueHandler {
|
||||
return &QueueHandler{queueService: queueService, logger: logger}
|
||||
}
|
||||
|
||||
// GetQueue returns the user's queue
|
||||
func (h *QueueHandler) GetQueue(c *gin.Context) {
|
||||
userID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q, err := h.queueService.GetOrCreateQueue(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get queue", err))
|
||||
return
|
||||
}
|
||||
RespondSuccess(c, http.StatusOK, gin.H{
|
||||
"queue": q,
|
||||
"items": q.Items,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateQueue updates the user's queue
|
||||
func (h *QueueHandler) UpdateQueue(c *gin.Context) {
|
||||
userID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req services.UpdateQueueRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondWithAppError(c, apperrors.NewValidationError("invalid request body"))
|
||||
return
|
||||
}
|
||||
q, err := h.queueService.UpdateQueue(c.Request.Context(), userID, &req)
|
||||
if err != nil {
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update queue", err))
|
||||
return
|
||||
}
|
||||
RespondSuccess(c, http.StatusOK, gin.H{
|
||||
"queue": q,
|
||||
"items": q.Items,
|
||||
})
|
||||
}
|
||||
|
||||
// AddQueueItemRequest represents the request body for adding an item
|
||||
type AddQueueItemRequest struct {
|
||||
TrackID uuid.UUID `json:"track_id" binding:"required"`
|
||||
}
|
||||
|
||||
// AddQueueItem adds a track to the queue
|
||||
func (h *QueueHandler) AddQueueItem(c *gin.Context) {
|
||||
userID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req AddQueueItemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondWithAppError(c, apperrors.NewValidationError("track_id is required"))
|
||||
return
|
||||
}
|
||||
item, err := h.queueService.AddToQueue(c.Request.Context(), userID, req.TrackID)
|
||||
if err != nil {
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add to queue", err))
|
||||
return
|
||||
}
|
||||
RespondSuccess(c, http.StatusCreated, gin.H{"item": item})
|
||||
}
|
||||
|
||||
// RemoveQueueItem removes an item from the queue
|
||||
func (h *QueueHandler) RemoveQueueItem(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 item ID"))
|
||||
return
|
||||
}
|
||||
if err := h.queueService.RemoveFromQueue(c.Request.Context(), userID, id); err != nil {
|
||||
RespondWithAppError(c, apperrors.NewNotFoundError("queue item not found"))
|
||||
return
|
||||
}
|
||||
RespondSuccess(c, http.StatusOK, gin.H{"message": "item removed"})
|
||||
}
|
||||
|
||||
// ClearQueue removes all items from the queue
|
||||
func (h *QueueHandler) ClearQueue(c *gin.Context) {
|
||||
userID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.queueService.ClearQueue(c.Request.Context(), userID); err != nil {
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to clear queue", err))
|
||||
return
|
||||
}
|
||||
RespondSuccess(c, http.StatusOK, gin.H{"message": "queue cleared"})
|
||||
}
|
||||
60
veza-backend-api/internal/models/queue.go
Normal file
60
veza-backend-api/internal/models/queue.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Queue represents a user's playback queue
|
||||
type Queue struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex" json:"user_id"`
|
||||
CurrentTrackID *uuid.UUID `gorm:"type:uuid" json:"current_track_id,omitempty"`
|
||||
CurrentPosition int `gorm:"not null;default:0" json:"current_position"`
|
||||
IsPlaying bool `gorm:"not null;default:false" json:"is_playing"`
|
||||
Shuffle bool `gorm:"not null;default:false" json:"shuffle"`
|
||||
RepeatMode string `gorm:"size:20;not null;default:off" json:"repeat_mode"`
|
||||
Volume int `gorm:"not null;default:100" json:"volume"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Items []QueueItem `gorm:"foreignKey:QueueID;constraint:OnDelete:CASCADE" json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// TableName defines the table name for GORM
|
||||
func (Queue) TableName() string {
|
||||
return "queues"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM hook to generate UUID if not set
|
||||
func (q *Queue) BeforeCreate(tx *gorm.DB) error {
|
||||
if q.ID == uuid.Nil {
|
||||
q.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueItem represents a track in a queue
|
||||
type QueueItem struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
QueueID uuid.UUID `gorm:"type:uuid;not null" json:"queue_id"`
|
||||
TrackID uuid.UUID `gorm:"type:uuid;not null" json:"track_id"`
|
||||
Position int `gorm:"not null" json:"position"`
|
||||
AddedAt time.Time `gorm:"autoCreateTime" json:"added_at"`
|
||||
|
||||
Track Track `gorm:"foreignKey:TrackID" json:"track,omitempty"`
|
||||
}
|
||||
|
||||
// TableName defines the table name for GORM
|
||||
func (QueueItem) TableName() string {
|
||||
return "queue_items"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM hook to generate UUID if not set
|
||||
func (qi *QueueItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if qi.ID == uuid.Nil {
|
||||
qi.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
139
veza-backend-api/internal/services/queue_service.go
Normal file
139
veza-backend-api/internal/services/queue_service.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/models"
|
||||
)
|
||||
|
||||
// QueueService handles user playback queue operations
|
||||
type QueueService struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewQueueService creates a new QueueService
|
||||
func NewQueueService(db *gorm.DB, logger *zap.Logger) *QueueService {
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
return &QueueService{db: db, logger: logger}
|
||||
}
|
||||
|
||||
// GetOrCreateQueue returns the user's queue, creating it if it doesn't exist
|
||||
func (s *QueueService) GetOrCreateQueue(ctx context.Context, userID uuid.UUID) (*models.Queue, error) {
|
||||
var q models.Queue
|
||||
err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&q).Error
|
||||
if err == nil {
|
||||
return s.loadQueueWithItems(ctx, &q)
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
q = models.Queue{UserID: userID}
|
||||
if err := s.db.WithContext(ctx).Create(&q).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.loadQueueWithItems(ctx, &q)
|
||||
}
|
||||
|
||||
func (s *QueueService) loadQueueWithItems(ctx context.Context, q *models.Queue) (*models.Queue, error) {
|
||||
var items []models.QueueItem
|
||||
if err := s.db.WithContext(ctx).Where("queue_id = ?", q.ID).Order("position ASC").Preload("Track").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.Items = items
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// UpdateQueue updates queue metadata (order, current position, etc.)
|
||||
func (s *QueueService) UpdateQueue(ctx context.Context, userID uuid.UUID, req *UpdateQueueRequest) (*models.Queue, error) {
|
||||
q, err := s.GetOrCreateQueue(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.CurrentTrackID != nil {
|
||||
q.CurrentTrackID = req.CurrentTrackID
|
||||
}
|
||||
if req.CurrentPosition != nil {
|
||||
q.CurrentPosition = *req.CurrentPosition
|
||||
}
|
||||
if req.IsPlaying != nil {
|
||||
q.IsPlaying = *req.IsPlaying
|
||||
}
|
||||
if req.Shuffle != nil {
|
||||
q.Shuffle = *req.Shuffle
|
||||
}
|
||||
if req.RepeatMode != nil {
|
||||
q.RepeatMode = *req.RepeatMode
|
||||
}
|
||||
if req.Volume != nil {
|
||||
q.Volume = *req.Volume
|
||||
}
|
||||
if req.ItemOrder != nil {
|
||||
for i, itemID := range req.ItemOrder {
|
||||
s.db.WithContext(ctx).Model(&models.QueueItem{}).Where("id = ? AND queue_id = ?", itemID, q.ID).Update("position", i)
|
||||
}
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Save(q).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.loadQueueWithItems(ctx, q)
|
||||
}
|
||||
|
||||
// UpdateQueueRequest represents the request body for updating a queue
|
||||
type UpdateQueueRequest struct {
|
||||
CurrentTrackID *uuid.UUID `json:"current_track_id"`
|
||||
CurrentPosition *int `json:"current_position"`
|
||||
IsPlaying *bool `json:"is_playing"`
|
||||
Shuffle *bool `json:"shuffle"`
|
||||
RepeatMode *string `json:"repeat_mode"`
|
||||
Volume *int `json:"volume"`
|
||||
ItemOrder []uuid.UUID `json:"item_order"`
|
||||
}
|
||||
|
||||
// AddToQueue adds a track to the user's queue
|
||||
func (s *QueueService) AddToQueue(ctx context.Context, userID uuid.UUID, trackID uuid.UUID) (*models.QueueItem, error) {
|
||||
q, err := s.GetOrCreateQueue(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var maxPos int
|
||||
s.db.WithContext(ctx).Model(&models.QueueItem{}).Where("queue_id = ?", q.ID).Select("COALESCE(MAX(position), -1)").Scan(&maxPos)
|
||||
item := models.QueueItem{QueueID: q.ID, TrackID: trackID, Position: maxPos + 1}
|
||||
if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.db.WithContext(ctx).Preload("Track").First(&item, item.ID)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// RemoveFromQueue removes an item from the queue
|
||||
func (s *QueueService) RemoveFromQueue(ctx context.Context, userID uuid.UUID, itemID uuid.UUID) error {
|
||||
q, err := s.GetOrCreateQueue(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := s.db.WithContext(ctx).Where("id = ? AND queue_id = ?", itemID, q.ID).Delete(&models.QueueItem{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearQueue removes all items from the user's queue
|
||||
func (s *QueueService) ClearQueue(ctx context.Context, userID uuid.UUID) error {
|
||||
q, err := s.GetOrCreateQueue(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.db.WithContext(ctx).Where("queue_id = ?", q.ID).Delete(&models.QueueItem{}).Error
|
||||
}
|
||||
Loading…
Reference in a new issue