diff --git a/docs/FEATURE_STATUS.md b/docs/FEATURE_STATUS.md index 93761d26e..76e4f39bf 100644 --- a/docs/FEATURE_STATUS.md +++ b/docs/FEATURE_STATUS.md @@ -34,7 +34,7 @@ Ce document décrit le statut réel des fonctionnalités par rapport au code. |---------|-------------|--------| | **Studio** (Cloud File Browser) | Supprimé | Dossier supprimé | | **Education** | Supprimé | Dossier supprimé | -| **Gamification** (achievements, leaderboard) | MSW handlers | api_manager (build ignore), MSW uniquement | +| **Gamification** (achievements, leaderboard) | MSW handlers | api_manager archivé (internal/api/archive/), MSW uniquement | ## Fonctionnalités désactivées (feature flags) diff --git a/veza-backend-api/docs/TODOS_AUDIT.md b/veza-backend-api/docs/TODOS_AUDIT.md index c1952e92b..e0ba8b7aa 100644 --- a/veza-backend-api/docs/TODOS_AUDIT.md +++ b/veza-backend-api/docs/TODOS_AUDIT.md @@ -100,10 +100,10 @@ This document lists all TODO, FIXME, HACK, and XXX comments found in the codebas - **Status**: Open - **Category**: Migration incomplete -### `internal/api/api_manager.go` +### `internal/api/archive/api_manager.go` (archived, build-ignored) - **TODO**: Réactiver api_manager.go après stabilisation du noyau et alignement des services (graphql, grpc, websocket, features) - **Priority**: P3 - - **Status**: Deferred + - **Status**: Deferred (fichier archivé) - **Category**: Future feature - **TODO**: Implement feature start - **Priority**: P3 diff --git a/veza-backend-api/internal/api/archive/README.md b/veza-backend-api/internal/api/archive/README.md new file mode 100644 index 000000000..04da9f630 --- /dev/null +++ b/veza-backend-api/internal/api/archive/README.md @@ -0,0 +1,10 @@ +# Archived API modules + +This directory contains deprecated or build-ignored API code. + +## api_manager.go + +- **Status**: Excluded from build (`//go:build ignore`) +- **Reason**: GraphQL, gRPC, WebSocket, and gamification (achievements, leaderboard) handlers were never fully integrated with the core REST router +- **Content**: handleGetAchievements, handleGetLeaderboard, multi-protocol setup +- **Reference**: docs/TODOS_AUDIT.md, docs/FEATURE_STATUS.md (Gamification: fantôme) diff --git a/veza-backend-api/internal/api/api_manager.go b/veza-backend-api/internal/api/archive/api_manager.go similarity index 100% rename from veza-backend-api/internal/api/api_manager.go rename to veza-backend-api/internal/api/archive/api_manager.go diff --git a/veza-backend-api/internal/core/track/handler.go b/veza-backend-api/internal/core/track/handler.go index b4dd2a7c0..1fe08d36b 100644 --- a/veza-backend-api/internal/core/track/handler.go +++ b/veza-backend-api/internal/core/track/handler.go @@ -1287,7 +1287,7 @@ func (h *TrackHandler) UnlikeTrack(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"message": "track unliked"}) + handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "track unliked"}) } // GetTrackLikes gère la récupération du nombre de likes d'un track @@ -1323,7 +1323,7 @@ func (h *TrackHandler) GetTrackLikes(c *gin.Context) { } } - c.JSON(http.StatusOK, gin.H{ + handlers.RespondSuccess(c, http.StatusOK, gin.H{ "count": count, "is_liked": isLiked, }) @@ -1663,12 +1663,12 @@ func (h *TrackHandler) CreateShare(c *gin.Context) { share, err := h.shareService.CreateShare(c.Request.Context(), trackID, userID, req.Permissions, req.ExpiresAt) if err != nil { - if errors.Is(err, ErrForbidden) { + if errors.Is(err, services.ErrForbidden) { // MOD-P2-003: Utiliser AppError au lieu de gin.H h.respondWithError(c, http.StatusForbidden, "forbidden") return } - if errors.Is(err, ErrTrackNotFound) { + if errors.Is(err, services.ErrTrackNotFound) { // MOD-P2-003: Utiliser AppError au lieu de gin.H h.respondWithError(c, http.StatusNotFound, "track not found") return @@ -1678,7 +1678,7 @@ func (h *TrackHandler) CreateShare(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"share": share}) + handlers.RespondSuccess(c, http.StatusOK, gin.H{"share": share}) } // GetSharedTrack récupère un track via son token de partage @@ -1992,6 +1992,10 @@ func (h *TrackHandler) RecordPlay(c *gin.Context) { // Enregistrer l'événement via le service err = h.playbackAnalyticsService.RecordPlayback(c.Request.Context(), analytics) if err != nil { + if errors.Is(err, services.ErrTrackNotFound) { + h.respondWithError(c, http.StatusNotFound, "track not found") + return + } h.respondWithError(c, http.StatusInternalServerError, "failed to record play event") return } diff --git a/veza-backend-api/internal/core/track/handler_additional_test.go b/veza-backend-api/internal/core/track/handler_additional_test.go index 585091b8c..55b501cb1 100644 --- a/veza-backend-api/internal/core/track/handler_additional_test.go +++ b/veza-backend-api/internal/core/track/handler_additional_test.go @@ -368,9 +368,12 @@ func TestTrackHandler_GetSharedTrack_Success(t *testing.T) { // TestTrackHandler_GetSharedTrack_InvalidToken tests GetSharedTrack with invalid token func TestTrackHandler_GetSharedTrack_InvalidToken(t *testing.T) { - handler, _, router, cleanup := setupTestTrackHandler(t) + handler, db, router, cleanup := setupTestTrackHandler(t) defer cleanup() + shareService := services.NewTrackShareService(db) + handler.SetShareService(shareService) + router.GET("/tracks/shared/:token", handler.GetSharedTrack) invalidToken := "invalid-token" @@ -473,9 +476,9 @@ func TestTrackHandler_GetUserLikedTracks_Success(t *testing.T) { c.Set("user_id", userID) c.Next() }) - router.GET("/users/me/liked", handler.GetUserLikedTracks) + router.GET("/users/:id/likes", handler.GetUserLikedTracks) - req := httptest.NewRequest(http.MethodGet, "/users/me/liked?page=1&limit=10", nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/likes?limit=10&offset=0", userID.String()), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) diff --git a/veza-backend-api/internal/core/track/handler_test.go b/veza-backend-api/internal/core/track/handler_test.go index 013ca586a..df41e1eae 100644 --- a/veza-backend-api/internal/core/track/handler_test.go +++ b/veza-backend-api/internal/core/track/handler_test.go @@ -44,6 +44,7 @@ func setupTestTrackHandler(t *testing.T) (*TrackHandler, *gorm.DB, *gin.Engine, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, + &models.PlaybackAnalytics{}, ) require.NoError(t, err) diff --git a/veza-backend-api/internal/services/job_service.go b/veza-backend-api/internal/services/job_service.go index 5e4368f10..b19e46475 100644 --- a/veza-backend-api/internal/services/job_service.go +++ b/veza-backend-api/internal/services/job_service.go @@ -14,6 +14,7 @@ type JobEnqueuer interface { EnqueueEmailJobWithTemplate(to, subject, templateName string, templateData map[string]interface{}) EnqueueThumbnailJob(inputPath, outputPath string, width, height int) EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) + EnqueueTranscodingJob(trackID uuid.UUID, inputPath, outputDir string) } // JobService gère les jobs en arrière-plan @@ -29,6 +30,7 @@ const ( TypeThumbnailGenerate = "thumbnail:generate" TypeAnalyticsProcess = "analytics:process" TypeWebhookDelivery = "webhook:delivery" + TypeTranscoding = "transcoding" ) // EmailPayload représente les données pour l'envoi d'email diff --git a/veza-backend-api/internal/services/job_service_test.go b/veza-backend-api/internal/services/job_service_test.go index 6203af7fb..b5ba6e15d 100644 --- a/veza-backend-api/internal/services/job_service_test.go +++ b/veza-backend-api/internal/services/job_service_test.go @@ -27,6 +27,8 @@ func (m *MockJobEnqueuer) EnqueueThumbnailJob(inputPath, outputPath string, widt m.Called(inputPath, outputPath, width, height) } +func (m *MockJobEnqueuer) EnqueueTranscodingJob(trackID uuid.UUID, inputPath, outputDir string) {} + func (m *MockJobEnqueuer) EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) { m.Called(eventName, userID, payload) } diff --git a/veza-backend-api/internal/services/playback_analytics_service.go b/veza-backend-api/internal/services/playback_analytics_service.go index 02a549f6a..d5c1fb277 100644 --- a/veza-backend-api/internal/services/playback_analytics_service.go +++ b/veza-backend-api/internal/services/playback_analytics_service.go @@ -2,14 +2,15 @@ package services import ( "context" + "errors" "fmt" - "github.com/google/uuid" "time" - "veza-backend-api/internal/models" - + "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" + + "veza-backend-api/internal/models" ) // PlaybackAnalyticsService gère les analytics de lecture de tracks @@ -82,8 +83,8 @@ func (s *PlaybackAnalyticsService) RecordPlayback(ctx context.Context, analytics // Vérifier que le track existe var track models.Track if err := s.db.WithContext(ctx).First(&track, analytics.TrackID).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return fmt.Errorf("track not found: %s", analytics.TrackID) + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrTrackNotFound } return fmt.Errorf("failed to get track: %w", err) } diff --git a/veza-backend-api/internal/workers/job_worker.go b/veza-backend-api/internal/workers/job_worker.go index ccc3be7da..cdfaf2d0a 100644 --- a/veza-backend-api/internal/workers/job_worker.go +++ b/veza-backend-api/internal/workers/job_worker.go @@ -280,6 +280,8 @@ func (w *JobWorker) executeJob(ctx context.Context, job Job) error { return w.processThumbnailJob(ctx, job) case "analytics": return w.processAnalyticsJob(ctx, job) + case services.TypeTranscoding: + return w.processTranscodingJob(ctx, job) default: return fmt.Errorf("unknown job type: %s", job.Type) } @@ -368,6 +370,20 @@ func (w *JobWorker) EnqueueThumbnailJob(inputPath, outputPath string, width, hei w.Enqueue(job) } +// EnqueueTranscodingJob enqueues a transcoding job (HLS/ffmpeg - stub implementation) +func (w *JobWorker) EnqueueTranscodingJob(trackID uuid.UUID, inputPath, outputDir string) { + job := Job{ + Type: services.TypeTranscoding, + Priority: 2, + Payload: map[string]interface{}{ + "track_id": trackID.String(), + "input_path": inputPath, + "output_dir": outputDir, + }, + } + w.Enqueue(job) +} + // EnqueueAnalyticsJob helper func (w *JobWorker) EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) { jobPayload := map[string]interface{}{ @@ -448,6 +464,25 @@ func (w *JobWorker) processAnalyticsJob(ctx context.Context, job Job) error { return analyticsJob.Execute(ctx, w.db, w.logger) } +// processTranscodingJob processes HLS/audio transcoding (stub - ffmpeg integration TODO) +func (w *JobWorker) processTranscodingJob(ctx context.Context, job Job) error { + p := job.Payload + trackIDStr, _ := p["track_id"].(string) + inputPath, _ := p["input_path"].(string) + outputDir, _ := p["output_dir"].(string) + + if trackIDStr == "" || inputPath == "" { + return fmt.Errorf("transcoding job missing track_id or input_path") + } + + // Stub: log and succeed. Actual ffmpeg/HLS transcoding to be implemented. + w.logger.Info("Transcoding job (stub)", + zap.String("track_id", trackIDStr), + zap.String("input_path", inputPath), + zap.String("output_dir", outputDir)) + return nil +} + // GetStats retourne les stats DB si possible func (w *JobWorker) GetStats() map[string]interface{} { var pending, processing, failed int64 diff --git a/veza-backend-api/migrations/081_create_playback_analytics.sql b/veza-backend-api/migrations/081_create_playback_analytics.sql new file mode 100644 index 000000000..ce7acd960 --- /dev/null +++ b/veza-backend-api/migrations/081_create_playback_analytics.sql @@ -0,0 +1,25 @@ +-- 081_create_playback_analytics.sql +-- Playback analytics for track listening events (BE-API-019) + +CREATE TABLE IF NOT EXISTS public.playback_analytics ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + track_id UUID NOT NULL REFERENCES public.tracks(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + + play_time INT NOT NULL DEFAULT 0, + pause_count INT NOT NULL DEFAULT 0, + seek_count INT NOT NULL DEFAULT 0, + completion_rate DECIMAL(5,2) NOT NULL DEFAULT 0, + + started_at TIMESTAMPTZ NOT NULL, + ended_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_playback_analytics_track_id ON public.playback_analytics(track_id); +CREATE INDEX IF NOT EXISTS idx_playback_analytics_user_id ON public.playback_analytics(user_id); +CREATE INDEX IF NOT EXISTS idx_playback_analytics_created_at ON public.playback_analytics(created_at); + +COMMENT ON TABLE public.playback_analytics IS 'Playback analytics for track listening events';