package services import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" "audio-catalyst/internal/domain/entities" ) // AudioBookService сервис для работы с аудиокнигами type AudioBookService struct{} // NewAudioBookService создает новый сервис аудиокниг func NewAudioBookService() *AudioBookService { return &AudioBookService{} } // CreateChapters создает главы из MP3 файлов func (s *AudioBookService) CreateChapters(mp3Files []string) []entities.Chapter { // Гарантируем пустой слайс, а не nil (в JSON будет [] вместо null) chapters := make([]entities.Chapter, 0, len(mp3Files)) // Сортировка файлов по имени sortedFiles := make([]string, len(mp3Files)) copy(sortedFiles, mp3Files) sort.Strings(sortedFiles) currentStart := 0.0 for i, file := range sortedFiles { fileName := filepath.Base(file) baseName := strings.TrimSuffix(fileName, ".mp3") chapterTitle := s.extractChapterTitle(baseName) // Оцениваем длительность файла в секундах durSec := s.estimateDuration(file) if durSec < 0 { durSec = 0 } chapter := entities.Chapter{ ID: i, Start: currentStart, End: currentStart + durSec, Title: chapterTitle, Duration: int(durSec), } currentStart += durSec chapters = append(chapters, chapter) } return chapters } // extractChapterTitle извлекает название главы из имени файла func (s *AudioBookService) extractChapterTitle(fileName string) string { name := strings.TrimSuffix(fileName, ".mp3") patterns := []string{ `^(\d{1,3})[\s\-_.]*(.*)$`, `^[Cc]hapter[\s_]*(\d+)(.*)$`, `^[Гг]лава[\s_]*(\d+)(.*)$`, `^[Чч]асть[\s_]*(\d+)(.*)$`, } for _, pattern := range patterns { re := regexp.MustCompile(pattern) if matches := re.FindStringSubmatch(name); len(matches) >= 2 { number := matches[1] title := "" if len(matches) >= 3 { title = strings.TrimSpace(matches[2]) title = strings.TrimLeft(title, "-_. ") } if title != "" { return fmt.Sprintf("%s - %s", number, title) } return number } } return name } // estimateDuration оценивает продолжительность MP3 файла (в секундах) // Приближение: 1 MB ≈ 0.86 минуты (≈51.6 секунды) при средних битрейтах func (s *AudioBookService) estimateDuration(filePath string) float64 { info, err := os.Stat(filePath) if err != nil { return 0 } // Размер файла в MB sizeMB := float64(info.Size()) / (1024.0 * 1024.0) // Примерная продолжительность в минутах estimatedMinutes := sizeMB * 0.86 return estimatedMinutes * 60.0 } // CleanTitle очищает название книги от технической информации func (s *AudioBookService) CleanTitle(title string) string { // Удаляем информацию в квадратных скобках в конце title = regexp.MustCompile(`\s*\[.*?\]\s*$`).ReplaceAllString(title, "") // Удаляем информацию в круглых скобках в конце title = regexp.MustCompile(`\s*\(.*?\)\s*$`).ReplaceAllString(title, "") // Удаляем технические обозначения techPatterns := []string{ `\s*MP3\s*$`, `\s*FLAC\s*$`, `\s*\d+\s*kbps\s*$`, `\s*\d+\s*кбит/с\s*$`, `\s*VBR\s*$`, `\s*CBR\s*$`, `\s*\d{4}\s*$`, } for _, pattern := range techPatterns { title = regexp.MustCompile(`(?i)`+pattern).ReplaceAllString(title, "") } // Убираем лишние пробелы title = strings.TrimSpace(title) title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ") return title }