package handlers import ( "bytes" "context" "encoding/csv" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "github.com/google/uuid" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" ) // PlaylistExportServiceInterface defines methods required by PlaylistExportHandler type PlaylistExportServiceInterface interface { GetPlaylist(ctx context.Context, playlistID uuid.UUID, userID *uuid.UUID) (*models.Playlist, error) CheckPermission(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, requiredPermission models.PlaylistPermission) (bool, error) } // PlaylistExportHandler gère les exports de playlists // T0493: Create Playlist Export Feature type PlaylistExportHandler struct { playlistService PlaylistExportServiceInterface } // NewPlaylistExportHandler crée un nouveau handler d'export de playlists func NewPlaylistExportHandler(playlistService *services.PlaylistService) *PlaylistExportHandler { return &PlaylistExportHandler{ playlistService: playlistService, } } // NewPlaylistExportHandlerWithInterface creates a new handler with interface for testing func NewPlaylistExportHandlerWithInterface(playlistService PlaylistExportServiceInterface) *PlaylistExportHandler { return &PlaylistExportHandler{ playlistService: playlistService, } } // ExportPlaylistJSON exporte une playlist au format JSON // T0493: Create Playlist Export Feature func (h *PlaylistExportHandler) ExportPlaylistJSON(c *gin.Context) { playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"}) return } // Vérifier que la playlist existe et que l'utilisateur a accès var userID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { userID = &uid } } playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID) if err != nil { if err.Error() == "playlist not found" { c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Vérifier que l'utilisateur a accès (propriétaire, collaborateur ou playlist publique) currentUserID := uuid.Nil if userID != nil { currentUserID = *userID } if playlist.UserID != currentUserID && !playlist.IsPublic { // Vérifier si l'utilisateur est collaborateur if userID != nil { hasAccess, err := h.playlistService.CheckPermission(c.Request.Context(), playlistID, *userID, models.PlaylistPermissionRead) if err != nil || !hasAccess { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } else { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } // Préparer les données d'export exportData := map[string]interface{}{ "playlist": map[string]interface{}{ "id": playlist.ID, "title": playlist.Title, "description": playlist.Description, "is_public": playlist.IsPublic, "cover_url": playlist.CoverURL, "track_count": playlist.TrackCount, "created_at": playlist.CreatedAt, "updated_at": playlist.UpdatedAt, }, "tracks": make([]map[string]interface{}, 0), "exported_at": time.Now().Format(time.RFC3339), } // Ajouter les tracks avec leurs informations if playlist.Tracks != nil { for _, playlistTrack := range playlist.Tracks { // Track est un struct (non-pointer), toujours valide { trackData := map[string]interface{}{ "position": playlistTrack.Position, "id": playlistTrack.Track.ID, "title": playlistTrack.Track.Title, "artist": playlistTrack.Track.Artist, "album": playlistTrack.Track.Album, "duration": playlistTrack.Track.Duration, "genre": playlistTrack.Track.Genre, "year": playlistTrack.Track.Year, "added_at": playlistTrack.AddedAt, } exportData["tracks"] = append(exportData["tracks"].([]map[string]interface{}), trackData) } } } // Convertir en JSON jsonData, err := json.MarshalIndent(exportData, "", " ") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate JSON export"}) return } // Définir les headers pour le téléchargement filename := "playlist_" + playlistID.String() + "_" + time.Now().Format("20060102") + ".json" // Changed to playlistID.String() c.Header("Content-Type", "application/json") c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, "application/json", jsonData) } // ExportPlaylistCSV exporte une playlist au format CSV // T0493: Create Playlist Export Feature func (h *PlaylistExportHandler) ExportPlaylistCSV(c *gin.Context) { playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"}) return } // Vérifier que la playlist existe et que l'utilisateur a accès var userID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { userID = &uid } } playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID) if err != nil { if err.Error() == "playlist not found" { c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Vérifier que l'utilisateur a accès (propriétaire, collaborateur ou playlist publique) currentUserID := uuid.Nil if userID != nil { currentUserID = *userID } if playlist.UserID != currentUserID && !playlist.IsPublic { // Vérifier si l'utilisateur est collaborateur if userID != nil { hasAccess, err := h.playlistService.CheckPermission(c.Request.Context(), playlistID, *userID, models.PlaylistPermissionRead) if err != nil || !hasAccess { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } else { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } // Créer le buffer CSV var csvData [][]string // En-têtes csvData = append(csvData, []string{ "Position", "Track ID", "Title", "Artist", "Album", "Duration (seconds)", "Genre", "Year", "Added At", }) // Ajouter les tracks if playlist.Tracks != nil { for _, playlistTrack := range playlist.Tracks { // Track est un struct (non-pointer), toujours valide { row := []string{ strconv.Itoa(playlistTrack.Position), playlistTrack.Track.ID.String(), // Changed to playlistTrack.Track.ID.String() playlistTrack.Track.Title, playlistTrack.Track.Artist, playlistTrack.Track.Album, strconv.Itoa(playlistTrack.Track.Duration), playlistTrack.Track.Genre, strconv.Itoa(playlistTrack.Track.Year), playlistTrack.AddedAt.Format(time.RFC3339), } csvData = append(csvData, row) } } } // Générer le CSV var csvBuffer bytes.Buffer writer := csv.NewWriter(&csvBuffer) // Écrire toutes les lignes for _, row := range csvData { if err := writer.Write(row); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate CSV export"}) return } } writer.Flush() if err := writer.Error(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate CSV export"}) return } // Définir les headers pour le téléchargement filename := "playlist_" + playlistID.String() + "_" + time.Now().Format("20060102") + ".csv" // Changed to playlistID.String() c.Header("Content-Type", "text/csv") c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, "text/csv", csvBuffer.Bytes()) } // ExportPlaylistM3U exporte une playlist au format M3U (v0.10.4 F145). // Each track gets an #EXTINF line and a URL (download or stream). func (h *PlaylistExportHandler) ExportPlaylistM3U(c *gin.Context) { playlistID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"}) return } var userID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { userID = &uid } } playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID) if err != nil { if err.Error() == "playlist not found" { c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } currentUserID := uuid.Nil if userID != nil { currentUserID = *userID } if playlist.UserID != currentUserID && !playlist.IsPublic { if userID != nil { hasAccess, err := h.playlistService.CheckPermission(c.Request.Context(), playlistID, *userID, models.PlaylistPermissionRead) if err != nil || !hasAccess { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } else { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } } // Build base URL for track URLs (download endpoint) scheme := "https" if c.GetHeader("X-Forwarded-Proto") == "http" || (c.Request.TLS == nil && !strings.Contains(c.Request.Host, "localhost")) { scheme = "http" } if strings.HasPrefix(c.Request.Host, "localhost") || strings.HasPrefix(c.Request.Host, "127.0.0.1") { scheme = "http" } baseURL := scheme + "://" + c.Request.Host if !strings.HasPrefix(baseURL, "http") { baseURL = "https://" + c.Request.Host } var buf bytes.Buffer buf.WriteString("#EXTM3U\n") if playlist.Tracks != nil { for _, pt := range playlist.Tracks { dur := pt.Track.Duration if dur <= 0 { dur = -1 } title := pt.Track.Title artist := pt.Track.Artist extInf := fmt.Sprintf("#EXTINF:%d,%s - %s\n", dur, artist, title) buf.WriteString(extInf) trackURL := fmt.Sprintf("%s/api/v1/tracks/%s/download\n", baseURL, pt.Track.ID.String()) buf.WriteString(trackURL) } } filename := "playlist_" + playlistID.String() + "_" + time.Now().Format("20060102") + ".m3u" c.Header("Content-Type", "audio/x-mpegurl") c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, "audio/x-mpegurl", buf.Bytes()) }