package services import ( "encoding/csv" "encoding/json" "fmt" "os" "path/filepath" "time" "veza-backend-api/internal/models" "go.uber.org/zap" ) // PlaybackExportService gère l'export des analytics de lecture // T0367: Create Playback Analytics Export Service type PlaybackExportService struct { logger *zap.Logger } // NewPlaybackExportService crée un nouveau service d'export d'analytics func NewPlaybackExportService(logger *zap.Logger) *PlaybackExportService { if logger == nil { logger = zap.NewNop() } return &PlaybackExportService{ logger: logger, } } // ExportFormat représente le format d'export type ExportFormat string const ( FormatCSV ExportFormat = "csv" FormatJSON ExportFormat = "json" ) // ExportCSV exporte les analytics en format CSV // T0367: Create Playback Analytics Export Service func (s *PlaybackExportService) ExportCSV(analytics []models.PlaybackAnalytics, filename string) error { if len(analytics) == 0 { return fmt.Errorf("no analytics data to export") } // Créer le répertoire parent si nécessaire if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } file, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() // Écrire l'en-tête CSV header := []string{ "ID", "Track ID", "User ID", "Play Time (seconds)", "Pause Count", "Seek Count", "Completion Rate (%)", "Started At", "Ended At", "Created At", } if err := writer.Write(header); err != nil { return fmt.Errorf("failed to write CSV header: %w", err) } // Écrire les données for _, a := range analytics { endedAt := "" if a.EndedAt != nil { endedAt = a.EndedAt.Format(time.RFC3339) } row := []string{ a.ID.String(), // UUID as string a.TrackID.String(), // UUID as string a.UserID.String(), // UUID as string fmt.Sprintf("%d", a.PlayTime), fmt.Sprintf("%d", a.PauseCount), fmt.Sprintf("%d", a.SeekCount), fmt.Sprintf("%.2f", a.CompletionRate), a.StartedAt.Format(time.RFC3339), endedAt, a.CreatedAt.Format(time.RFC3339), } if err := writer.Write(row); err != nil { return fmt.Errorf("failed to write CSV row: %w", err) } } s.logger.Info("Analytics exported to CSV", zap.String("filename", filename), zap.Int("count", len(analytics))) return nil } // ExportJSON exporte les analytics en format JSON // T0367: Create Playback Analytics Export Service func (s *PlaybackExportService) ExportJSON(analytics []models.PlaybackAnalytics, filename string) error { if len(analytics) == 0 { return fmt.Errorf("no analytics data to export") } // Créer le répertoire parent si nécessaire if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } data, err := json.MarshalIndent(analytics, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } if err := os.WriteFile(filename, data, 0644); err != nil { return fmt.Errorf("failed to write JSON file: %w", err) } s.logger.Info("Analytics exported to JSON", zap.String("filename", filename), zap.Int("count", len(analytics))) return nil } // ExportReport génère un rapport d'analytics avec statistiques agrégées // T0367: Create Playback Analytics Export Service func (s *PlaybackExportService) ExportReport(analytics []models.PlaybackAnalytics, filename string, format ExportFormat) error { if len(analytics) == 0 { return fmt.Errorf("no analytics data to export") } // Calculer les statistiques stats := s.calculateReportStats(analytics) // Générer le rapport selon le format switch format { case FormatCSV: return s.exportReportCSV(analytics, stats, filename) case FormatJSON: return s.exportReportJSON(analytics, stats, filename) default: return fmt.Errorf("unsupported export format: %s", format) } } // ReportStats représente les statistiques d'un rapport type ReportStats struct { TotalSessions int `json:"total_sessions"` TotalPlayTime int64 `json:"total_play_time"` // seconds AveragePlayTime float64 `json:"average_play_time"` // seconds TotalPauses int64 `json:"total_pauses"` AveragePauses float64 `json:"average_pauses"` TotalSeeks int64 `json:"total_seeks"` AverageSeeks float64 `json:"average_seeks"` AverageCompletion float64 `json:"average_completion"` // percentage CompletionRate float64 `json:"completion_rate"` // percentage of sessions with >90% completion CompletedSessions int `json:"completed_sessions"` // sessions with ≥95% completion } // calculateReportStats calcule les statistiques agrégées func (s *PlaybackExportService) calculateReportStats(analytics []models.PlaybackAnalytics) ReportStats { stats := ReportStats{ TotalSessions: len(analytics), } if len(analytics) == 0 { return stats } var totalPlayTime int64 var totalPauses int64 var totalSeeks int64 var totalCompletion float64 completedSessions := 0 for _, a := range analytics { totalPlayTime += int64(a.PlayTime) totalPauses += int64(a.PauseCount) totalSeeks += int64(a.SeekCount) totalCompletion += a.CompletionRate if a.CompletionRate >= 95.0 { completedSessions++ } } stats.TotalPlayTime = totalPlayTime stats.AveragePlayTime = float64(totalPlayTime) / float64(len(analytics)) stats.TotalPauses = totalPauses stats.AveragePauses = float64(totalPauses) / float64(len(analytics)) stats.TotalSeeks = totalSeeks stats.AverageSeeks = float64(totalSeeks) / float64(len(analytics)) stats.AverageCompletion = totalCompletion / float64(len(analytics)) stats.CompletedSessions = completedSessions // Completion rate (sessions with >90% completion) sessionsOver90 := 0 for _, a := range analytics { if a.CompletionRate >= 90.0 { sessionsOver90++ } } if len(analytics) > 0 { stats.CompletionRate = float64(sessionsOver90) / float64(len(analytics)) * 100.0 } return stats } // exportReportCSV exporte un rapport en CSV avec statistiques func (s *PlaybackExportService) exportReportCSV(analytics []models.PlaybackAnalytics, stats ReportStats, filename string) error { // Créer le répertoire parent si nécessaire if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } file, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() // Écrire les statistiques statsHeader := []string{"Statistic", "Value"} if err := writer.Write(statsHeader); err != nil { return fmt.Errorf("failed to write stats header: %w", err) } statsRows := [][]string{ {"Total Sessions", fmt.Sprintf("%d", stats.TotalSessions)}, {"Total Play Time (seconds)", fmt.Sprintf("%d", stats.TotalPlayTime)}, {"Average Play Time (seconds)", fmt.Sprintf("%.2f", stats.AveragePlayTime)}, {"Total Pauses", fmt.Sprintf("%d", stats.TotalPauses)}, {"Average Pauses", fmt.Sprintf("%.2f", stats.AveragePauses)}, {"Total Seeks", fmt.Sprintf("%d", stats.TotalSeeks)}, {"Average Seeks", fmt.Sprintf("%.2f", stats.AverageSeeks)}, {"Average Completion (%)", fmt.Sprintf("%.2f", stats.AverageCompletion)}, {"Completion Rate (%)", fmt.Sprintf("%.2f", stats.CompletionRate)}, {"Completed Sessions (≥95%)", fmt.Sprintf("%d", stats.CompletedSessions)}, } for _, row := range statsRows { if err := writer.Write(row); err != nil { return fmt.Errorf("failed to write stats row: %w", err) } } // Ligne vide if err := writer.Write([]string{}); err != nil { return fmt.Errorf("failed to write empty row: %w", err) } // Écrire l'en-tête des données header := []string{ "ID", "Track ID", "User ID", "Play Time (seconds)", "Pause Count", "Seek Count", "Completion Rate (%)", "Started At", "Ended At", "Created At", } if err := writer.Write(header); err != nil { return fmt.Errorf("failed to write CSV header: %w", err) } // Écrire les données for _, a := range analytics { endedAt := "" if a.EndedAt != nil { endedAt = a.EndedAt.Format(time.RFC3339) } row := []string{ a.ID.String(), // UUID as string a.TrackID.String(), // UUID as string a.UserID.String(), // UUID as string fmt.Sprintf("%d", a.PlayTime), fmt.Sprintf("%d", a.PauseCount), fmt.Sprintf("%d", a.SeekCount), fmt.Sprintf("%.2f", a.CompletionRate), a.StartedAt.Format(time.RFC3339), endedAt, a.CreatedAt.Format(time.RFC3339), } if err := writer.Write(row); err != nil { return fmt.Errorf("failed to write CSV row: %w", err) } } s.logger.Info("Analytics report exported to CSV", zap.String("filename", filename), zap.Int("count", len(analytics))) return nil } // exportReportJSON exporte un rapport en JSON avec statistiques func (s *PlaybackExportService) exportReportJSON(analytics []models.PlaybackAnalytics, stats ReportStats, filename string) error { // Créer le répertoire parent si nécessaire if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } // Structure du rapport report := map[string]interface{}{ "generated_at": time.Now().Format(time.RFC3339), "statistics": stats, "analytics": analytics, } data, err := json.MarshalIndent(report, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } if err := os.WriteFile(filename, data, 0644); err != nil { return fmt.Errorf("failed to write JSON file: %w", err) } s.logger.Info("Analytics report exported to JSON", zap.String("filename", filename), zap.Int("count", len(analytics))) return nil } // ExportToWriter exporte les analytics vers un writer (pour streaming HTTP) func (s *PlaybackExportService) ExportToWriter(analytics []models.PlaybackAnalytics, format ExportFormat, writer interface{}) error { switch format { case FormatCSV: return s.exportCSVToWriter(analytics, writer) case FormatJSON: return s.exportJSONToWriter(analytics, writer) default: return fmt.Errorf("unsupported export format: %s", format) } } // exportCSVToWriter exporte en CSV vers un writer func (s *PlaybackExportService) exportCSVToWriter(analytics []models.PlaybackAnalytics, writer interface{}) error { // Cette méthode peut être étendue pour supporter différents types de writers // Pour l'instant, on retourne une erreur si le writer n'est pas un *os.File file, ok := writer.(*os.File) if !ok { return fmt.Errorf("writer must be *os.File for CSV export") } csvWriter := csv.NewWriter(file) defer csvWriter.Flush() // Écrire l'en-tête header := []string{ "ID", "Track ID", "User ID", "Play Time (seconds)", "Pause Count", "Seek Count", "Completion Rate (%)", "Started At", "Ended At", "Created At", } if err := csvWriter.Write(header); err != nil { return fmt.Errorf("failed to write CSV header: %w", err) } // Écrire les données for _, a := range analytics { endedAt := "" if a.EndedAt != nil { endedAt = a.EndedAt.Format(time.RFC3339) } row := []string{ a.ID.String(), // UUID as string a.TrackID.String(), // UUID as string a.UserID.String(), // UUID as string fmt.Sprintf("%d", a.PlayTime), fmt.Sprintf("%d", a.PauseCount), fmt.Sprintf("%d", a.SeekCount), fmt.Sprintf("%.2f", a.CompletionRate), a.StartedAt.Format(time.RFC3339), endedAt, a.CreatedAt.Format(time.RFC3339), } if err := csvWriter.Write(row); err != nil { return fmt.Errorf("failed to write CSV row: %w", err) } } return nil } // exportJSONToWriter exporte en JSON vers un writer func (s *PlaybackExportService) exportJSONToWriter(analytics []models.PlaybackAnalytics, writer interface{}) error { // Cette méthode peut être étendue pour supporter différents types de writers // Pour l'instant, on retourne une erreur si le writer n'est pas un *os.File file, ok := writer.(*os.File) if !ok { return fmt.Errorf("writer must be *os.File for JSON export") } encoder := json.NewEncoder(file) encoder.SetIndent("", " ") return encoder.Encode(analytics) }