- Backend: BPM and MusicalKey in Track model, UpdateTrack handler - track_search_service: enable BPM filter (min_bpm, max_bpm) - Frontend: Track type, UpdateTrackParams, display in TrackDetailPageInfo - TrackMetadataEditModal: BPM input, edit flow for track creator - MSW: bpm, musical_key in mock track, correct response envelope
181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// 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}
|
|
}
|
|
|
|
// NewTrackSearchServiceWithDB crée un service de recherche avec support read replica
|
|
func NewTrackSearchServiceWithDB(db *database.Database) *TrackSearchService {
|
|
return &TrackSearchService{db: db.ForRead()}
|
|
}
|
|
|
|
// 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 (E1: BPM in Track model)
|
|
if params.MinBPM != nil && params.MaxBPM != nil {
|
|
if *params.MinBPM <= *params.MaxBPM {
|
|
query = query.Where("bpm >= ? AND bpm <= ?", *params.MinBPM, *params.MaxBPM)
|
|
}
|
|
} else if params.MinBPM != nil {
|
|
query = query.Where("bpm >= ?", *params.MinBPM)
|
|
} else if params.MaxBPM != nil {
|
|
query = query.Where("bpm <= ?", *params.MaxBPM)
|
|
}
|
|
|
|
// 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
|
|
}
|