veza/veza-backend-api/internal/handlers/hls_handler.go
senke ef386e0ae3 fix(backend): commit swagger annotation pass + missing handler methods
routes_users.go (already on main) calls settingsHandler.GetPreferences /
UpdatePreferences and gdprExportHandler.ExportJSON, but the methods only
existed in the working tree — main wouldn't compile, so deploy.yml's
build-backend job was stuck on the same compile error every run.

Bundles the WIP swagger annotation sweep across chat / marketplace /
role / settings / gdpr / etc. handlers with the regenerated swagger.json,
swagger.yaml, docs.go and openapi.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:16:57 +02:00

253 lines
8.9 KiB
Go

package handlers
import (
"context"
"net/http"
"github.com/google/uuid"
// "strconv" // Removed this import
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
)
// HLSServiceInterface defines methods needed for HLS handler
type HLSServiceInterface interface {
GetMasterPlaylist(ctx context.Context, trackID uuid.UUID) (string, error)
GetQualityPlaylist(ctx context.Context, trackID uuid.UUID, bitrate string) (string, error)
GetSegmentPath(ctx context.Context, trackID uuid.UUID, bitrate string, segment string) (string, error)
GetStreamInfo(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error)
GetStreamStatus(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error)
TriggerTranscodeQueue(ctx context.Context, trackID uuid.UUID, userID uuid.UUID) (uuid.UUID, error)
}
// HLSHandler gère les requêtes pour servir les fichiers HLS
type HLSHandler struct {
hlsService HLSServiceInterface
}
// NewHLSHandler crée un nouveau handler HLS
func NewHLSHandler(hlsService *services.HLSService) *HLSHandler {
return NewHLSHandlerWithInterface(hlsService)
}
// NewHLSHandlerWithInterface crée un nouveau handler HLS avec une interface (pour tests)
func NewHLSHandlerWithInterface(hlsService HLSServiceInterface) *HLSHandler {
return &HLSHandler{hlsService: hlsService}
}
// ServeMasterPlaylist sert le master playlist pour un track
// @Summary Get HLS master playlist
// @Description Serve the master M3U8 playlist for a track, containing all available qualities.
// @Tags Streaming
// @Produce application/vnd.apple.mpegurl
// @Param id path string true "Track ID"
// @Success 200 {string} string "#EXTM3U..."
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Router /api/v1/tracks/{id}/hls/master.m3u8 [get]
func (h *HLSHandler) ServeMasterPlaylist(c *gin.Context) {
trackID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
playlist, err := h.hlsService.GetMasterPlaylist(c.Request.Context(), trackID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
return
}
c.Header("Content-Type", "application/vnd.apple.mpegurl")
// v1.0.9 W3 Day 13 — playlists CAN change (live streams, transcoder
// retries, bitrate ladder edits) so 60s TTL is the right balance :
// edges still cache aggressively but a stale manifest only blocks
// the viewer briefly.
c.Header("Cache-Control", "public, max-age=60")
c.Header("Vary", "Accept-Encoding")
c.String(http.StatusOK, playlist)
}
// ServeQualityPlaylist sert une quality playlist pour un track et bitrate
// @Summary Get HLS quality playlist
// @Description Serve a specific quality M3U8 playlist for a track.
// @Tags Streaming
// @Produce application/vnd.apple.mpegurl
// @Param id path string true "Track ID"
// @Param bitrate path string true "Bitrate (e.g. 128k)"
// @Success 200 {string} string "#EXTM3U..."
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Router /api/v1/tracks/{id}/hls/{bitrate}/index.m3u8 [get]
func (h *HLSHandler) ServeQualityPlaylist(c *gin.Context) {
trackID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
bitrate := c.Param("bitrate")
playlist, err := h.hlsService.GetQualityPlaylist(c.Request.Context(), trackID, bitrate)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
return
}
c.Header("Content-Type", "application/vnd.apple.mpegurl")
// v1.0.9 W3 Day 13 — playlists CAN change (live streams, transcoder
// retries, bitrate ladder edits) so 60s TTL is the right balance :
// edges still cache aggressively but a stale manifest only blocks
// the viewer briefly.
c.Header("Cache-Control", "public, max-age=60")
c.Header("Vary", "Accept-Encoding")
c.String(http.StatusOK, playlist)
}
// ServeSegment sert un segment pour un track, bitrate et nom de segment
// @Summary Get HLS segment
// @Description Serve a specific video/audio segment (.ts) for a track and quality.
// @Tags Streaming
// @Produce video/mp2t
// @Param id path string true "Track ID"
// @Param bitrate path string true "Bitrate"
// @Param segment path string true "Segment name"
// @Success 200 {file} file "MPEG-TS segment"
// @Failure 404 {object} handlers.APIResponse "Segment not found"
// @Router /api/v1/tracks/{id}/hls/{bitrate}/{segment} [get]
func (h *HLSHandler) ServeSegment(c *gin.Context) {
trackID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
bitrate := c.Param("bitrate")
segment := c.Param("segment")
segmentPath, err := h.hlsService.GetSegmentPath(c.Request.Context(), trackID, bitrate, segment)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "segment not found"})
return
}
c.Header("Content-Type", "video/mp2t")
// v1.0.9 W3 Day 13 — segments are content-addressed (filename
// includes a hash). max-age=86400 + immutable lets every layer
// (browser, CDN, origin) skip re-validation entirely. If a segment
// regenerates after a re-encode, its filename changes — so this
// directive is safe.
c.Header("Cache-Control", "public, max-age=86400, immutable")
c.Header("Vary", "Accept-Encoding")
c.File(segmentPath)
}
// GetStreamInfo retourne les informations générales d'un stream HLS pour un track
// GET /api/v1/tracks/:id/hls/info
// BE-API-020: Implement HLS stream info endpoint
// @Summary Get HLS stream info
// @Description Get metadata about the HLS stream (bitrates, codecs).
// @Tags Streaming
// @Accept json
// @Produce json
// @Param id path string true "Track ID"
// @Success 200 {object} object
// @Failure 404 {object} handlers.APIResponse "Stream not found"
// @Router /api/v1/tracks/{id}/hls/info [get]
func (h *HLSHandler) GetStreamInfo(c *gin.Context) {
trackID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
return
}
info, err := h.hlsService.GetStreamInfo(c.Request.Context(), trackID)
if err != nil {
if err.Error()[:20] == "HLS stream not found" {
RespondWithAppError(c, apperrors.NewNotFoundError("HLS stream"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get stream info", err))
return
}
RespondSuccess(c, http.StatusOK, info)
}
// GetStreamStatus retourne le statut d'un stream HLS pour un track
// GET /api/v1/tracks/:id/hls/status
// BE-API-020: Implement HLS stream info endpoint
// @Summary Get HLS stream status
// @Description Get current transcoding status (pending, processing, ready).
// @Tags Streaming
// @Accept json
// @Produce json
// @Param id path string true "Track ID"
// @Success 200 {object} object
// @Failure 404 {object} handlers.APIResponse "Stream not found"
// @Router /api/v1/tracks/{id}/hls/status [get]
func (h *HLSHandler) GetStreamStatus(c *gin.Context) {
trackID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
return
}
status, err := h.hlsService.GetStreamStatus(c.Request.Context(), trackID)
if err != nil {
if err.Error()[:20] == "HLS stream not found" {
RespondWithAppError(c, apperrors.NewNotFoundError("HLS stream"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get stream status", err))
return
}
RespondSuccess(c, http.StatusOK, status)
}
// TriggerTranscode déclenche le transcodage HLS d'un track via la queue (T0343)
// @Summary Trigger HLS transcode
// @Description Manually start or restart the HLS transcoding process for a track.
// @Tags Streaming
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Success 202 {object} object{job_id=string}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Router /api/v1/tracks/{id}/hls/transcode [post]
func (h *HLSHandler) TriggerTranscode(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
}
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
jobID, err := h.hlsService.TriggerTranscodeQueue(c.Request.Context(), trackID, userID)
if err != nil {
if err.Error() == "track not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
if err.Error() == "forbidden: user does not own this track" {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"job_id": jobID})
}