- Реализовать тесты для поиска MP3-файлов и переименования/организации папок книг в репозитории файловой системы. - Создать FileLogger для записи сообщений в файл с поддержкой различных уровней журналирования и управления размером файлов. - Разработать репозиторий RuTracker для обработки поиска торрентов, получения метаданных и загрузки торрент-файлов. - Добавить тесты для нормализации URL в репозиторий RuTracker. - Реализовать адаптер логгера TUI для отображения логов в терминальном интерфейсе и, при необходимости, для записи логов в базовый логгер. - Создать менеджер TUI для управления пользовательским интерфейсом приложения, включая главное меню, экран обработки, настройки и отображение результатов.
133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
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
|
||
}
|