veza/veza-backend-api/internal/services/playback_export_service.go
2025-12-16 11:23:49 -05:00

426 lines
12 KiB
Go

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)
}