package controllers import ( "errors" "fmt" "log" "net/http" "strconv" "strings" "clapclap/internal/apperr" "clapclap/internal/dtos" "clapclap/internal/env" "clapclap/internal/ginctx" "clapclap/internal/models" "clapclap/internal/repositories" "clapclap/internal/upload" "clapclap/internal/utils" "clapclap/internal/uuid" "github.com/gin-gonic/gin" ) // CreateTrack godoc // @Summary Create or Finalize Track // @Description Saves the uploaded track metadata to the database after the S3 upload finishes. // @Tags tracks // @Accept json // @Produce json // @Param track body dtos.CreateTrackRequest true "Track details" // @Success 201 {object} models.Track // @Failure 400 {object} swaggError400 // @Failure 401 {object} swaggError401 // @Failure 403 {object} swaggError403 // @Failure 422 {object} swaggError422 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /tracks [post] // @Router /tracks/finalize-upload [post] func CreateTrack(c *gin.Context) { ownerID, _ := ginctx.GetUUID(c, "userID") s3ic, _ := ginctx.GetS3(c, "s3-internal") req, _ := ginctx.GetDto[dtos.CreateTrackRequest](c) repo, _ := ginctx.GetRepo(c, "repo") // TODO (pipo) : check for the error ffs genreID, _ := uuid.Parse(req.GenreID) // TODO: we should do this when reading the db, not when writing !!! // store only the path on write, resolve to complete url when returning // to client audioFileUrl := strings.TrimRight(env.C.AWS_ENDPOINT_URL_S3, "/") + "/" + env.C.BUCKET_NAME + "/" + strings.TrimLeft(req.ObjectKey, "/") track := models.Track{ Name: req.Name, AudioFile: audioFileUrl, GenreID: genreID, OwnerID: ownerID, IsPublic: req.IsPublic, } if err := repo.CreateTrack(&track); err != nil { log.Println("An error occurred when trying to create a new track: ", err) upload.DeleteS3Object(c.Request.Context(), s3ic, env.C.BUCKET_NAME, req.ObjectKey) apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to create track")) return } fullTrack, _ := repo.GetTrackByID(track.ID.String(), utils.GetReqPreloads(c.Request, utils.GetTrackPreloads()), ownerID) c.JSON(http.StatusCreated, *fullTrack) } // GetAllTracks godoc // @Summary Get all tracks // @Description Retrieves a paginated list of all public tracks // @Tags tracks // @Accept json // @Produce json // @Param q query string false "Search query string" // @Param genre_id query string false "Filter by Genre UUID" // <--- Added this line // @Param limit query int false "Limit (default 10)" // @Param page query int false "Page number (default 1)" // @Param sort_by query string false "Sort by column (default created_at)" Enums(name, created_at, random) // @Param order query string false "Order direction (default DESC)" Enums(ASC, DESC) // @Param preload query string false "Relation to preload" Enums(Owner, Comments, Ratings, Genre) // @Success 200 {array} models.Track // @Failure 400 {object} swaggError400 // @Failure 500 {object} swaggError500 // @Router /tracks [get] func GetAllTracks(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") searchQuery := c.Query("q") genreID := c.Query("genre_id") visibility := c.Query("visibility") // "public", "private", or empty limitStr := c.DefaultQuery("limit", "10") pageStr := c.DefaultQuery("page", "1") sortBy := c.DefaultQuery("sort_by", "created_at") order := c.DefaultQuery("order", "DESC") // 1. Identify if the user is a guest or logged in var userUUID uuid.UUID if uid, err := ginctx.GetUUID(c, "userID"); err == nil { userUUID = uid // Logged in! } else { userUUID = uuid.Nil // Guest! } limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 || limit > env.C.MAX_PAGE_SIZE { apperr.AbortWithError(c, apperr.NewBadRequestError(err, "Invalid limit parameter")) return } page, err := strconv.Atoi(pageStr) if err != nil || page <= 0 { apperr.AbortWithError(c, apperr.NewBadRequestError(err, "Invalid page parameter")) return } tracks, err := repo.GetAllTracks(limit, page, searchQuery, genreID, sortBy, order, utils.GetReqPreloads(c.Request, utils.GetTrackPreloads()), visibility, userUUID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to fetch tracks")) return } c.JSON(http.StatusOK, tracks) } // GetTrackByID godoc // @Summary Get a track by ID // @Description Retrieves a track's details using its UUID // @Tags tracks // @Produce json // @Param id path string true "Track UUID" // @Param preload query string false "Relation to preload" Enums(Owner, Comments, Ratings, Genre) // @Success 200 {object} models.Track // @Failure 404 {object} swaggError404 // @Router /tracks/{id} [get] func GetTrackByID(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") id := c.Param("id") var userUUID uuid.UUID if uid, err := ginctx.GetUUID(c, "userID"); err == nil { userUUID = uid // Logged in! } else { userUUID = uuid.Nil // Guest! } // 2. Pass the userUUID to the repository track, err := repo.GetTrackByID(id, utils.GetReqPreloads(c.Request, utils.GetTrackPreloads()), userUUID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Track not found or you do not have permission to view it")) return } c.JSON(http.StatusOK, *track) } func EnsureUserOwnsTrack(c *gin.Context) { uid, _ := ginctx.GetUUID(c, "userID") repo, _ := ginctx.GetRepo(c, "repo") req, _ := ginctx.GetDto[dtos.GetPrivateTrackUploadUrl](c) audioFileUrl := fmt.Sprintf("%s/%s/%s", strings.TrimRight(env.C.AWS_ENDPOINT_URL_S3, "/"), env.C.BUCKET_NAME, strings.TrimLeft(req.AudioFile, "/"), ) isOwned := repo.IsOwned(audioFileUrl, uid) if !isOwned { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "User does not own this track")) return } c.Next() } // UpdateTrack godoc // @Summary Update a track // @Description Updates a track's name or cover image // @Tags tracks // @Accept json // @Produce json // @Param id path string true "Track UUID" // @Param track body dtos.UpdateTrackRequest true "Update Payload" // @Success 200 {object} models.Track // @Failure 400 {object} swaggError400 // @Failure 401 {object} swaggError401 // @Failure 403 {object} swaggError403 // @Failure 404 {object} swaggError404 // @Failure 422 {object} swaggError422 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /tracks/{id} [put] // In internal/controllers/track.go func UpdateTrack(c *gin.Context) { userID, _ := ginctx.GetUUID(c, "userID") req, _ := ginctx.GetDto[dtos.UpdateTrackRequest](c) repo, _ := ginctx.GetRepo(c, "repo") id := c.Param("id") existingTrack, err := repo.GetTrackByID(id, nil, userID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Track not found")) return } if existingTrack.OwnerID != userID { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "You do not have permission to update this track")) return } if req.Name != "" { existingTrack.Name = req.Name } if req.ObjectKey != "" { existingTrack.CoverImage = strings.TrimRight(env.C.AWS_ENDPOINT_URL_S3, "/") + "/" + env.C.BUCKET_NAME + "/" + req.ObjectKey } else if req.ObjectKey == "" { apperr.AbortWithError(c, apperr.NewBadRequestError(errors.New("empty key"), "Object key cannot be empty")) return } if err := repo.UpdateTrack(existingTrack); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to update track")) return } c.JSON(http.StatusOK, *existingTrack) } // DeleteTrack godoc // @Summary Delete a single track version // @Description Deletes a track and automatically re-links and renumbers the remaining versions. // @Tags tracks // @Produce json // @Param id path string true "Track UUID" // @Success 200 {object} swaggMessageResponse // @Failure 401 {object} swaggError401 // @Failure 403 {object} swaggError403 // @Failure 404 {object} swaggError404 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /tracks/{id} [delete] func DeleteTrack(c *gin.Context) { id := c.Param("id") userID, _ := ginctx.GetUUID(c, "userID") repo, _ := ginctx.GetRepo(c, "repo") track, err := repo.GetTrackByID(id, nil, userID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "This track doesn't exist")) return } if track.OwnerID != userID { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "You do not have permission to delete this track")) return } // ========================================== // EXECUTING ALL DB CHANGES IN A TRANSACTION // ========================================== err = repo.RunInTx(func(txRepo *repositories.Repo) error { if err := txRepo.DeleteTrack(id); err != nil { return err } return nil }) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to update version history in database")) return } c.JSON(http.StatusOK, gin.H{"message": "Track successfully deleted"}) } // GetTracksByOwner godoc // @Summary Get tracks by owner // @Description Retrieves all tracks owned by a specific user UUID // @Tags tracks // @Produce json // @Param userID path string true "User UUID" // @Param preload query string false "Relation to preload" Enums(Owner, Comments, Ratings, Genre) // @Success 200 {array} models.Track // @Failure 500 {object} swaggError500 // @Router /tracks/user/{userID} [get] func GetTracksByOwner(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") userID := c.Param("userID") preloads := utils.GetReqPreloads(c.Request, utils.GetTrackPreloads()) tracks, err := repo.GetTracksByOwnerID(userID, preloads) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to fetch owner tracks")) return } c.JSON(http.StatusOK, tracks) } // GetTracksByGenre godoc // @Summary Get tracks by genre // @Description Retrieves a paginated list of tracks associated with a specific genre UUID // @Tags tracks // @Produce json // @Param genreID path string true "Genre UUID" // @Param limit query int false "Limit (default 10)" // @Param page query int false "Page number (default 1)" // @Param preload query string false "Relation to preload" Enums(Owner, Comments, Ratings, Genre) // @Success 200 {array} models.Track // @Failure 400 {object} swaggError400 // @Failure 500 {object} swaggError500 // @Router /tracks/genre/{genreID} [get] func GetTracksByGenre(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") // TODO (pipo) : do this using dto middleware var q struct { Limit int `form:"limit,default=10" binding:"gte=1"` Page int `form:"page,default=1" binding:"gte=1"` } if err := c.ShouldBindQuery(&q); err != nil { apperr.AbortWithError(c, apperr.NewBadRequestError(err, "Invalid query parameters")) return } genreID := c.Param("genreID") preloads := utils.GetReqPreloads(c.Request, utils.GetTrackPreloads()) tracks, err := repo.GetTracksByGenre(q.Limit, q.Page, preloads, genreID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to fetch tracks")) return } c.JSON(http.StatusOK, tracks) } // GetMaxTrackPages godoc // @Summary Get max track pages // @Description Calculates the total number of pages based on a given limit, optional search query, and optional genre filter // @Tags tracks // @Produce json // @Param limit query int false "Limit per page (default 10)" // @Param q query string false "Search query filter" // @Param genre_id query string false "Filter by Genre UUID" // @Success 200 {object} dtos.MaxPagesResponse // @Failure 400 {object} swaggError400 // @Failure 500 {object} swaggError500 // @Router /tracks/max-pages [get] func GetMaxTrackPages(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") searchQuery := c.Query("q") genreID := c.Query("genre_id") visibility := c.Query("visibility") // NEW limitStr := c.DefaultQuery("limit", "10") // 1. Identify if the user is a guest or logged in var userUUID uuid.UUID if uid, err := ginctx.GetUUID(c, "userID"); err == nil { userUUID = uid // Logged in! } else { userUUID = uuid.Nil // Guest! } limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 || limit > env.C.MAX_PAGE_SIZE { apperr.AbortWithError(c, apperr.NewBadRequestError(err, "Invalid limit")) return } totalCount, err := repo.CountTracks(searchQuery, genreID, visibility, userUUID) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to count tracks")) return } maxPages := int((totalCount + int64(limit) - 1) / int64(limit)) if maxPages == 0 { maxPages = 1 } c.JSON(http.StatusOK, dtos.MaxPagesResponse{ MaxPages: maxPages, TotalCount: totalCount, }) }