veza/veza-backend-api/internal/handlers/playlist_export_handler.go

327 lines
10 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package handlers
import (
"bytes"
"context"
2025-12-03 19:29:37 +00:00
"encoding/csv"
"encoding/json"
"fmt"
2025-12-03 19:29:37 +00:00
"net/http"
"strconv"
"strings"
2025-12-03 19:29:37 +00:00
"time"
"github.com/google/uuid"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
2025-12-03 19:29:37 +00:00
)
// 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)
}
2025-12-03 19:29:37 +00:00
// PlaylistExportHandler gère les exports de playlists
// T0493: Create Playlist Export Feature
type PlaylistExportHandler struct {
playlistService PlaylistExportServiceInterface
2025-12-03 19:29:37 +00:00
}
// 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,
}
}
2025-12-03 19:29:37 +00:00
// 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())
}