package services import ( "context" "fmt" "strings" "time" "gorm.io/gorm" "veza-backend-api/internal/models" ) // TrackSearchParams représente les paramètres de recherche de tracks type TrackSearchParams struct { Query string Tags []string TagMode string // "AND" or "OR" MinDuration *int // seconds MaxDuration *int // seconds MinBPM *int MaxBPM *int Genre *string Format *string MinDate *string // ISO date MaxDate *string // ISO date Page int Limit int SortBy string SortOrder string } // TrackSearchService gère la recherche avancée de tracks type TrackSearchService struct { db *gorm.DB } // NewTrackSearchService crée un nouveau service de recherche de tracks func NewTrackSearchService(db *gorm.DB) *TrackSearchService { return &TrackSearchService{db: db} } // SearchTracks effectue une recherche avancée de tracks avec support de filtres combinés func (s *TrackSearchService) SearchTracks(ctx context.Context, params TrackSearchParams) ([]*models.Track, int64, error) { query := s.db.Model(&models.Track{}).Where("is_public = ? AND deleted_at IS NULL", true) // Full-text search on title, artist, album if params.Query != "" { searchTerm := "%" + strings.ToLower(params.Query) + "%" query = query.Where( "LOWER(title) LIKE ? OR LOWER(artist) LIKE ? OR LOWER(album) LIKE ?", searchTerm, searchTerm, searchTerm, ) } // Tag search - Note: Tags field not in current model, skipping for now // This can be implemented when tags are added to the Track model if len(params.Tags) > 0 { // Tags functionality would go here when Tags field is added // For now, we'll skip tag filtering } // Duration filter (supports combined min/max) if params.MinDuration != nil && params.MaxDuration != nil { // Validate that min <= max if *params.MinDuration <= *params.MaxDuration { query = query.Where("duration >= ? AND duration <= ?", *params.MinDuration, *params.MaxDuration) } } else if params.MinDuration != nil { query = query.Where("duration >= ?", *params.MinDuration) } else if params.MaxDuration != nil { query = query.Where("duration <= ?", *params.MaxDuration) } // BPM filter - Note: BPM field not in current model, skipping for now // This can be implemented when BPM field is added to the Track model if params.MinBPM != nil || params.MaxBPM != nil { // BPM functionality would go here when BPM field is added // When implemented, should support combined min/max like duration } // Genre filter (case-insensitive) if params.Genre != nil && *params.Genre != "" { query = query.Where("LOWER(genre) = ?", strings.ToLower(strings.TrimSpace(*params.Genre))) } // Format filter (case-insensitive) if params.Format != nil && *params.Format != "" { query = query.Where("LOWER(format) = ?", strings.ToLower(strings.TrimSpace(*params.Format))) } // Date range filter (supports combined min/max) if params.MinDate != nil && *params.MinDate != "" { minDate, err := time.Parse(time.RFC3339, *params.MinDate) if err == nil { query = query.Where("created_at >= ?", minDate) } } if params.MaxDate != nil && *params.MaxDate != "" { maxDate, err := time.Parse(time.RFC3339, *params.MaxDate) if err == nil { query = query.Where("created_at <= ?", maxDate) } } // Count total before pagination var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("failed to count tracks: %w", err) } // Apply sorting with computed fields sortOrder := "DESC" if params.SortOrder == "asc" { sortOrder = "ASC" } sortBy := params.SortBy if sortBy == "" { sortBy = "created_at" } // Handle different sorting options switch sortBy { case "popularity": // Sort by like_count (popularity) query = query.Order(fmt.Sprintf("like_count %s", sortOrder)) case "play_count": // Sort by play_count (total plays) query = query.Order(fmt.Sprintf("play_count %s", sortOrder)) case "comment_count": // Sort by number of comments (requires join and count) query = query.Select("tracks.*, COALESCE(comment_counts.count, 0) as comment_count"). Joins("LEFT JOIN (SELECT track_id, COUNT(*) as count FROM track_comments WHERE deleted_at IS NULL GROUP BY track_id) as comment_counts ON comment_counts.track_id = tracks.id"). Order(fmt.Sprintf("comment_count %s", sortOrder)) case "title": // Sort by title alphabetically (case-insensitive) query = query.Order(fmt.Sprintf("LOWER(title) %s", sortOrder)) case "artist": // Sort by artist alphabetically (case-insensitive) query = query.Order(fmt.Sprintf("LOWER(artist) %s", sortOrder)) case "created_at", "updated_at", "duration": // Direct field sorting query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder)) case "like_count": // Sort by like_count (same as popularity) query = query.Order(fmt.Sprintf("like_count %s", sortOrder)) default: // Default to created_at query = query.Order(fmt.Sprintf("created_at %s", sortOrder)) } // Apply pagination if params.Page < 1 { params.Page = 1 } if params.Limit < 1 { params.Limit = 20 } if params.Limit > 100 { params.Limit = 100 // Max limit } offset := (params.Page - 1) * params.Limit query = query.Offset(offset).Limit(params.Limit) var tracks []*models.Track if err := query.Find(&tracks).Error; err != nil { return nil, 0, fmt.Errorf("failed to search tracks: %w", err) } return tracks, total, nil }